操作系统的笔记

进程管理

进程的概念与状态

进程是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。它由程序块、进程控制块(PCB)和数据块三部分组成。

进程与程序的区别:进程是程序的一次执行过程,没有程序就没有进程。

进程是动态,程序是静态

程序是完成某个特定功能的一系列程序语句的集合,只要不被破坏,它就永远存在。程序是一个静态的概念,而进程是一个动态的概念,它由创建而产生,完成任务后因撤销而消亡;进程是系统进行资源分配和调度的独立单位,而程序不是。

进程的同步与互斥

同步:直接的制约关系,速度有差异。【李四到终点需要等待张三到达】

互斥:间接制约关系,争夺临界资源。

PV操作

临界资源:诸进程间需要互斥方式对其进行共享的资源,如打印机、磁带机等
临界区:每个进程中访问临界资源的那段代码称为临界区
信号量:是一种特殊的变量
P是荷兰语的Passeren,V是荷兰语的Verhoog.

P操作就是申请资源操作,V就是释放资源操作。S就是信号量,若资源小于0,就需要进入阻塞队列。V操作资源数大于0就需要处理阻塞进程队列的资源。

同步模型

例题理解

答案:A,C

死锁问题

进程管理是操作系统的核心,但如果设计不当,就会出现死锁的问题。如果一个进程在等待一件不可能发生的事,则进程就死锁了。而如果一个或多个进程产生死锁,就会造成系统死锁。

下面例题

基于分配的所需的系统资源数少1,随后相加起来,如图即3*5+1=16个系统资源

形成死锁的4个必要条件

image8

有序资源分配法:需要多少系统资源给多少,比较浪费资源

银行家算法:分配资源的原则

  • 当一个进程对资源的最大需求量不超过系统中的资源数时可以接纳该进程。
  • 进程可以分期请求资源,但请求的总数不能超过最大需求量。
  • 当系统现有的资源不能满足进程尚需资源数时,对进程的请求可以推迟分配,但总能使进程在有限的时间里得到资源。

银行家算法例子

image9

image10

存储管理

页式存储

将程序与内存划分为同样大小的快,以页为单位将程序调入内存。

优点:利用率高,碎片小,分配及管理简单

缺点:增加了系统开销;可能产生抖动现象

image11

段页式存储

段式与页式的综合体,先分段,再分页,1个程序有若千个段,每个段中可以有若干页,每个页的大小相同,但每个段的大小不同。

image12

优点:空间浪费小,存储共享容易,存储保护容易,能动态连接

缺点:由于管理软件的增加,复杂性和开销也随之增加,需要的硬件以及占用的内容也有所增加,使得执行速度大大下降

页面置换算法

  • 最优(Optimal,OPT)算法
  • 随机(RAND)算法
  • √先进先出(FIFO)算法:有可能产生“抖动”。例如,432143543215序列,用3个页面,比4个缺页要少
  • 最近最少使用(LRU)算法:不会“抖动”,LRU的理论依据是“局部性原理”。
  • 时间局部性:刚被访问的内容,立即又被访问。
  • 空间局部性:刚被访问的内容,临近的空间很快被访问。

磁盘管理

存取时间=寻道时间+等待时间,寻道时间是指磁头移动到磁道所需的时间;等待时间为等待读写的扇区转到磁头下方所用的时间。

磁盘调度算法

  • 先来先服务(FCFS)
  • 最短寻道时间优先(SSTF)
  • 扫描算法(SCAN)
  • 循环扫描(CSCAN)算法

读取磁盘数据时间计算

读取磁盘数据的时间应包括以下三个部分:

  1. 找磁道的时间。
  2. 找块(扇区)的时间,即旋转延迟时间。
  3. 传输时间。

例题

某磁盘磁头从一个磁道移至另一个磁道需要10ms,文件在磁盘上非连续存放,逻辑上相邻数据块的平均移动距离为10个磁道,每块的旋转延迟时及传输时间分别为100ms和2ms,则读取一个100块的文件需要__ms时间。

A.10200 B.11000 C.11200 D.20200

1
2
3
这种问题先计算磁道移动缩需要的延迟即10*10
然后加上每块区域旋转的延迟时间+每个数据块传输的时间
最后再乘以总的读取文件总数。即[(10*10)+100+2]*100

作业管理

# 作业状态与作业管理

image13

作业的调度算法

  • 先来先服务法:作业先来先执行

  • 时间片轮转法:根据CPU的时间来核算

  • 短作业优先法:执行执行时间最短的作业优先执行

  • 最高优先权优先法:按照作业的优先权先后排列

  • 高响应比优先法:根据响应比(作业的等待时间/作业的执行时间)最高的优先执行

文件管理

索引文件结构

image14

  • 0-9:使用直接索引,每一个物理块为1k,也就是10k(0-10239)
  • 10:一级间接索引,存一页即1KB用4个存地址,即256个(1024/4),256每个对应1K,即256K
  • 11:二级间接索引,256个乘以256个乘以256K 即64M
  • 12:三级间接索引,256乘以二级间接索引,即16G

image15

树形目录结构

linux数据目录结构:/

就是文件夹目录,相对目录(不包括本身),绝对目录(从头写到尾)

空闲存储空间的管理

位示图法:类似于电影院订票,可以清晰的看出

设备管理

数据传输控制方式

(速度从低到高)

  • 程序控制(查询)方式:分为无条件传送和程序查询方式两种。方法简单,硬件开销小,但I/O能力不高,严重影响CPU的利用率
  • 程序中断方式:与程序控制方式相比,中断方式因为CPU无需等待而提高了传输请求的响应速度。
  • DMA方式:DMA方式是为了在主存与外设之间实现高速、批量数据交换而设置的。DMA方式比程序控制方式与中断方式都高效(与CPU没关系,CPU只负责下指令)
  • 通道方式
  • I/O处理机

# 虚设备与SPOOLING技术

排队式

image16

base64 image

[TOC]

计算机系统基础知识

计算机系统硬件基本储存

计算机系统是有软硬件组成

计算机的基本硬件系统是由:运算器、控制器、存储器、输入设备和输出设备5大部件组成。

运算器、控制器等部件被系统集成在一起统称为中央处理器单元(CPU)。CPU是硬件系统核心。

中央处理单元(CPU)

中央处理器单元(CPU)是计算机系统的核心部件,他负责获取程序指令、对指令进行译码并加以执行

CPU的功能

程序控制(控制器)

CPU通过执行指令来控制程序的执行顺序,这是CPU的重要功能

操作控制(控制器)

一条指令功能的实现需要若干操作型号配合来完成,CPU产生每条指令操作信号并将操作信号送往对应的部件,控制相应的部件按指令的功能进行操作

时间控制(运算器)

CPU对各种操作进行时间上的控制,即指令执行过程中操作信号的出现时间、持续时间及出现的时间顺序都需要进行严格控制。

数据处理(运算器)

CPU通过对数据进行算术运算及逻辑运算等方式进行加工处理,数据加工处理的结果被人们所利用。所以,对数据的加工处理也是CPU最根本的任务。

运算器

运算器由算术逻辑单元、累加寄存器、数据缓冲寄存器和状态条件寄存器组成。

运算器有两个主要功能:

  1. 执行所有的算术运算,例如加、减、乘、除等基本运算及附加运算。
  2. 执行所有的逻辑运算并进行逻辑测试,例如与、或、非、零值测试或两个值的比较等。

运算器中各组成部件的功能:

算术逻辑单元ALU:

ALU是运算器的重要组成部件,负责处理数据,实现对数据的算术运算和逻辑运算,

累加寄存器AC:

AC通常简称为累加器,他是一个通用寄存器,其功能当运算器的算术逻辑单元执行算术或逻辑运算时,为ALU提供一个工作区。

例如,在执行一个减法运算前,先将被减树取出暂存在AC中,再从内储存器中取出减数,然后同AC的内容相减,将所得的结果送回AC中,运算的结果是放在累加器中的,运算器中至少要有一个累加寄存器。

数据缓冲寄存器DR:

在对内存储器进行读/写操作时,用DR暂时存放由内存储存器读/写的一条指令或一个数据

DR的主要作用为:作为CPU和内存、外部设备之间数据传送的中转站;作为CPU和内存、外围设备之间在操作速度上的缓冲:在单累加器结构的运算器中,数据缓冲寄存器还可兼作为操作数寄存器

状态条件寄存器PSW:

PSW保存有算术指令和逻辑指令运行或测试的结果建立的各种条件码内容。

保存了当前指令执行完成之后的状态,通常,一个算术操作产生一个运算结果,而一个逻辑操作产生一个判决。

控制器

控制器用于控制整个CPU的工作,他决定了计算机运行过程的自动化。他不仅要保证程序的正确执行,而且还要能够处理异常事件。

指令控制逻辑要完成取指令、分析指令和执行指令的操作,其过程分为取指令、指令译码、按指令操作码执行、形成下一条指令地址等步骤

控制器中各组成部件的功能:

指令寄存器IR:

存放的是从内存中取得指令,就像个中间站一样,不过是存放指令的中间站

当CPU执行一条指令时,先把他从内存存储器取到缓冲寄存器中,在送入IR暂存,指令译码器根据IR的内容产生各种微操作指令控制其他的组成部件工作。

程序计数器PC:

存放的是指令的地址,还有计数的功能

PC具有寄存信息和计数两种功能,又称为指令计数器

在程序开始执行前,将程序的起始地址送入PC,该地址在程序加载到内存时确定,因此PC的内容即是程序第一条指令的地址。执行指令时,CPU自动修改PC的内容,以便使其保持的总是将要执行的下一条指令的地址。由于大多数指令都是按顺序来执行的,所以修改的过程通常只是简单地对PC加1。当遇到传移指令时,后继指令的地址根据当前指令的地址加上一个向前或向后转移的位移量得到,或者根据转移指令给出的直接转移的地址得到。

地址寄存器AR:

存放的是cpu访问内存单元的地址

AR保存当前CPU所访问的内存单元地址。

指令译码器ID:

是把操作码解析成对应的指令操作

指令包含操作码和地址两个部分,为了能执行任何给定的指令,必须对操作码进行缝隙,指令译码器就是对指令中的操作码字段进行缝隙解释,识别该指令规定的操作,向操作控制器发出具体的控制器信号,控制各部件工作,完成所需的功能。

计算机基本单位

单位名称 英文表述
(比特)*[最小数据单位]* b/bit
字节 [与b相差8倍] [最小存储单位] B/byte
千字节 [与B相差1024倍] KB
兆字节 [与KB相差1024倍] MB
吉字节 [与MB相差1024倍] GB
太字节 [与GB相差1024倍] TB

数据的表示

进制转换

R->10

R进制转十进制使用按权展开法,其具体的操作方式为:将R进制数每一位数值用
$$
R^k
$$
来表示,k与该为何小数点之间的距离有关,当该位位于小数点左边,k的值是该位数的小数点之间的数码的个数

当该位数位于小数点右边,k值是负值,其绝对值是该位和小数点之间码数的个数+1

*例如 二进制:10100.01 = 1*2^4 + 1*2^2 + 1*2^-2

*例如 七进制:604.01 = 6*7^2 + 4*7^0 + 1*7^-2

10->R

十进制转R进制使用短除法

例如将94转换为2进制数*(取余数(除到最后的数小于进制))

94 47 23 11 5 2 1
除进制 2 2 2 2 2 2
余数 0 1 1 1 1 0 1

然后反取得:1011110

2->8

$$
2^3 = 8
$$

*举例 10001110 转8进制

拆分为

10 001 110
2^1 2^0 2^2+2^1
2 1 6

所以得出结果为216
$$
O216 或
(216)_8
$$

2->16

$$
2^4 = 16
$$

*举例 1001110 转16进制

拆分为

1000 1110
2^3 2^3+2^2+2^1
8 8+4+2=14 (A,B,C,D,E)
8 E

所以转换为16进制为8E
$$
( 8E)_{16}或OX8E或8EH
$$

进制得加减法

加法遇n进一,减法借1当n

原码,反码,补码,移码

机器自查为8位2进制数,第一位为符号位:0表示正数,1表示负数 ;后七位表示数

数值1 数值-1 1-1【1+(-1)】
原码 0000 0001 1000 0001 1000 0010(左边两个相加)
反码 0000 0001(一样) 1111 1110 (除符号位,其他全变) 1111 1111(左边两个相加)
补码 0000 0001(一样) 1111 1111(在反码的基础上+1) 0000 0000(左边两个相加)
移码 1000 0001(在补码基础上,变化符号位) 0111 1111(补发基础上,变化符号位) 1000 0000

**只有补码能加减计算

数据表示范围

补码和移码都没有-0

image-20220918101942613.png image-20220918101357424.png

浮点数

$$
N= 尾数*基数^{指数}
$$

$$
举例:3.14*10^3
$$

其中的

3代表的就是阶码

特点

  1. 一般尾数用补码,阶码用移码
  2. 阶码的位数决定数的表示范围,位数越多表示范围越大
  3. 尾数的位数决定数的有效精度,位数越多精度越高
  4. 对阶时,小数向大数看齐
  5. 对阶是通过比较小数的尾数右移实现的

运算过程

对阶》尾数计算》结果格式化

存储格式

3.14*10^3

阶符 阶码 数符 尾数
0(阶码为正:0) 0(数字的正负)

浮点数。当机器字长为n时,定点数的补码和移码可表示2个数,而其原码和反码只 能表示2”-1个数(0的表示占用了两个编码),因此,定点数所能表示的数值范围比较小,在 运算中很容易因结果超出范围而溢出。浮点数是小数点位置不固定的数,它能表示更大范围 的数。

Flynn计算机体系结构分类

体系结构类型 结构 代表
单指令流单数据SISD 控制部分:1
处理器:1
主存模块:1
单处理器系统
单指令流多数据流SIMD 控制部分:1
处理器:N
主存模块:N
并行处理机
阵列处理机
超级向量处理机
多指令流单数据流MISD 控制部分:N
处理器:1
主存模块:N
目前没有,有理论流水线计算机
多指令流多数据流MIMD 控制部分:N
处理器:N
主存模块:N
多处理机系统
多计算机

指令的基本概念

一条指令就是机器语言的一个语句,它是一组有意义的二进制代码,指令的基本格式如下:

例如a+b=c

操作码字段 地址码字段 地址码字段 地址码字段
+ a b c
操作码字段 地址码字段 地址码字段
+ a b

例如自增就只需一个地址码字段

操作码部分指出了计算机要执行什么性质的操作,如加法、减法、取数、存数等。地址码字段需要包含各操作数的地址及操作结果的存放地址等,从其地址结构的角度可以分为三地址指令、二地址指令、一地址指令和零地址指令。

寻址方式

名称 特点
立即寻址方式 操作数直接在指令中,速度快,灵活性差
直接寻址方式 指令中存放的是操作数的地址
间接寻址放肆 指令中存放了一个地址,这个地址对应的内容是操作数的地址
寄存器寻址方式 寄存器存放操作数
寄存器间接寻址方式 寄存器内存放的是操作数的地址
image3519aa493e77cd08.png

CISC与RISC

指令系统类型 指令 寻址方式 实现方式 其他
CISC(复杂) 数量多,使用频率差别大,可变长格式 支持多种 微程序控制技术(微码) 研制周期长
RISC(精简) 数量少,使用频率接近,定长格式,大部分为单周期指令,操作寄存器,只有Load/Store操作内存 支持方式少 增加了通用寄存器;硬布线逻辑控制为主适合采用流水线 优化编译,有效支持高级语言

CISC:复杂,指令数量多,频率差别大,多寻址
RISC:精简,指令数量少,操作奇存器,单周期,少寻址,多通用寄存器,流水线

流水线

概念

流水线是指在程序执行时多条指令重量进行操作的一种准并行处理实现技术。各种部件同时处理是针对不同指令而言的,它们可同时为多条指令的不同部分进行工作,以提高各部件的利用率和指令的平均执行速度

imagea442034d665d11fb.png

流水线计算公式:

流水线周期为执行最长的一段
$$
1条指令执行时间+(指令条数-1)*流水线周期
$$

  1. 理论公式

$$
(t1+t2+…+tk)+(n-1)*Δt
$$

  1. 实践公式(理论公式没答案,使用实现公式)
    $$
    k*Δt+(n-1)*Δt
    $$image3eed5a15a68a300b.png

超标量流水线

imagead3f86bc2e51fe00.png

多了一个概念,度,本图度为2

吞吐率计算

流水线的吞吐率(Though Put rate,TP)是指在单位时间内流水线所完成的任务数量或输出的结果数量,计算流水线吞吐率的最基本的公式如下:
$$
TP=\frac{指令条数}{流水线执行时间}
$$
流水线最大吞吐率:
$$
TP_{max}=\lim_{n\to\infty}\frac{n}{(k+n-1)\Delta t}=\frac{1}{\Delta t}
$$

存储结构

名称 描述 速度(容量)
CPU 寄存器 最快(容量最小)成本高
Cache 按内容存取 快(容量较小)
内存(主存) 随机存储器(RAM)
只读存储器(ROM)
较慢(容量大)
外存(辅存) 硬盘,光盘,U盘等 最慢(容量最大)

Cache

在计算机的存储系统体系中,Cache是访问速度最快的层次(若有寄存器,则寄存器最快)。

使用Cache改善系统性能的依据是程序的局部性原理:

  1. 时间局部性:前后续访问不会被淘汰
  2. 空间局部性:相邻空间可以被范围

如果以h代表对Cache的访问命中率,t1表示Cache的周期时间,t表示主存储器周期时间,以读操作为例,使用“Cache+主存储器”的系统的平均周期为t3,则:
$$
t_3 = h*t_1+(1-h)*t_2
$$
其中,(1-h)又称为失效率(未命中率)。

映像

  1. 直接相邻映像:硬件电路较简单,但冲突率高

    (0页只能放在0页中,重复的旧页将会被淘汰,所以冲突率高)

  2. 全相联映像:电路难于设计和实现,只适合于小容量的cache,冲突率较低(cache里面的每一页,都会存储主存里的每一页)

  3. 组相联映像:直接相联与全相联的折中

    (先分区后分组,0组放0组,组内随便放,对管理系统的消耗很大)

    映像

地址映像是将主存与Cache的存储空间划分为若干大小相同的页(称为块)。
例如,某机的主存容量为1GB,划分为2048页,每页512KB;Cache容量为8MB,划分为16页,每页512KB。

编址与计算

存储单元
按字编址:存储体的存储单元是字存储单元,即最小寻址单位是一个字
按字节编址:存储体的存储单元是字节存储单元;即最小寻址单位是一个字节(Byte=8位bit)。

记住公式:
$$
总片数=总容量/每片的容量
$$
例:若内存地址区间为4000H~43FFH,每个存储单元可存储16位二进制数,该内存区域用4片存储器芯片构成,则构成该内存所用的存储器芯片的容量是多少?

1
2
3
4
5
43FFH-4000H+1=4400H-4000H=400H
一位16进制 对应4位2进制 所以00H位8位
4化为2进制0100 得出2位
8+2=10位
(2^10*16(bit)位二进制数)/4片存储器=256*16bit

总线

一条总线同一时刻仅允许一个设备发送,但允许多个设备接收。
总线的分类:

  1. 数据总线(Data Bus):在CPU与RAM之间来回传送需要处理或是需要储存的数据。
  2. 地址总线(Address Bus):用来指定在RAM(Random Access Memory)之中储存的数据的地址。
  3. 控制总线(ControlBus):将微处理器控制单元(ControlUnit)的信号,传送到周边设备,一般常见的为USB Bus和1394 Bus。

串联系统和并联系统

一般求可靠性

串联:可靠性*可靠性……

并联:1-(1-可靠性)*(1-可靠性)……

N模混合系统

校验码

码距:任何一种编码都由许多码字构成,任意两个码字之间最少变化的二进制位数就称为数据校验码的码距。
例如,用4位二进制表示16种状态,则有16个不同的码字,此时码距为1。如0000与0001。(换几个码距就是几个)

奇偶校验

奇偶校验码的编码方法是:由若干位有效信息(如一个字节),再加上一个二进制位(校验位)组成校验码。

  1. 奇校验:整个校验码(有效信息位和校验位)中“1”的个数为奇数。

  2. 偶校验:整个校验码(有效信息位和校验位)中“1”的个数为偶数。

奇偶校验,可检查1位的错误,不可纠错。

例如:1010

校验码【1010(1)】 的1就为奇数

如果这是传输后的校验码为【1011(1)】,即1为偶数了,就发现数据发生错误,但如果两位发送错误,如【1111(1)】那么就检测不出来了。

循环冗余校验码CRC

CRC校验,可检错,不可纠错。
CRC的编码方法是:在k位信息码之后拼接r位校验码。应用CRC码的关键是如何从k位信息位简便地得到r位校验位(编码),以及如何从k+r位信息码判断是否出错。
循环穴余校验码编码规律如下:

  1. 把待编码的N位有效信息表示为多项式M(X);

  2. 把M(X)左移K位,得到M(X)×XK,这样空出了K位,以便拼装K位余数(即校验位);

  3. 选取一个K+1位的产生多项式G(X),对M(X)×X做模2除;

  4. 把左移K位以后的有效信息与余数R(X)做模2加减,拼接为CRC码,此时的CRC码共有N+K位。

把接收到的CRC码用约定的生成多项式G(X)去除,如果正确,则余数为0;如果某一位出错,则余数不为0。不同的位数出错其余数不同,余数和出错位序号之间有惟一的对应关系。

image

这个的意思就是指,先除两个数,然后将余数补在原数后面发送,数据除不尽即说明数据发生了错误

海明校验码

海明校验,可检错,也可纠错。
海明校验码的原理是:在有效信息位中加入几个校验位形成海明码使码距比较均匀地拉大,并把海明码的每个二进制位分配到几个奇偶校验组中,当某一位出错后,就会引起有关的几个校验位的值发生变化,这不但可以发现错误,还能指出错误的位置,为自动纠错提供了依据
$$
2^r\geq m+r+1
$$

计算机基础知识例题

例题

进制转换题

①.2015年下

image-20220918151931192.png

②.2017年下

image-20220918154932132.png

答案

进制转换题

①.2015年下

DABFFH-B3000+1

加一得原因就是因为存储容量必须包含B3000H这个数

根据后面得H可知该数为16进制数

被减数 D A B F F
减数 B 3 0 0 0
2 7 B F F
再加+1 2 7 C 0 0

因为选项中没有带H,所以需要将数转为十进制数。

2 7 C 0 0
2*16^4 7*16^3 12*16^2 0*16^1 0*16^0

其中的2*16^4也可以化简为:
$$
16^4=(2^4)^4=2^{16}
$$
因为我们得出来得数的单位为最小存储单位B,所以需要将最后的结果/1024,也就是除以:
$$
2^{10}
$$
以此类推,得

2*16^4 7*16^3 12*16^2 0*16^1 0*16^0
化简 2*2^16 7*2^12 12*2^8 0 0
除以2^10 2*2^6 7*2^2 12*2^-2 0 0

相加得:
$$
22^6+72^2+122^{-2}=264+74+12\frac{1}{4}=128+28+3=159(KB)
$$
所以结果就是B:159KB

②.2017年下

因为2015年下已经表述很清楚了,后面就简写

DFFFF+1-A0000,E0000-A0000=40000

转为10(D)进制数 4*16^4

好,接下来就是重点了

因为内存按字节编地址(所以我们求出来的数的单位为B/Btye),用存储容量为32K*8bit的存储器芯片构成地址:

所以可以得出:
$$
x32K8bit=416^4Btye
$$
因为*8Btye=8bit,1K=1024B=2^10B

转换单位为:
$$
x32K8bit=\frac{4*2^{16}}{2^{10}}8bit=256K8bit
$$

$$
x=\frac{256K8bit}{32K8bit}
$$

求x为8,所以答案为B:8

程序设计语言

低级语言和高级语言

计算机硬件只能识别由0、1组成的机器指令序列,即机器指令程序,因此机器指令是最基本的计算机语言。

例如,用ADD表示加法、用SUB表示减法等。用符号表示的指令称为汇编指令,汇编指令的集合被称为汇编语言。

人们称机器语言和汇编语言为低级语言。

面向各类应用的程序程序设计语言为高级语言。(JAVA、C、C++、PHP、Python、Delphi、PASCAL等)。

编译程序和解释程序

语言之间的翻译形式有多种,基本方式为汇编、解释和编译

用某种高级语言或汇编语言编写的程序称为源程序,源程序不能直接在计算机上执行。如果源程序是用汇编语言编写的,则需要一个汇编程序将其翻译成目标程序后才能执行。如果源程序是用某种高级语言编写的,则需要对应的解释程序或编译程序对其进行翻译,然后在机器上运行。

解释程序也称为解释器,它或者直接解释执行源程序,或者将源程序翻译成某种中间代码后再加以执行;

  • 而编译程序(编译器)则是将源程序翻译成目标语言程序,然后在计算机上运行目标程序。这两种语言处理程序的根本区别是:
  • 而在解释方式下,解释程序和源程序(或其某种等价表示)要参与到程序的运行过程中,运行程序的控制权在解释程序。

简单来说,在解释方式下,翻译源程序时不生成独立的目标程序,而编译器则将源程序翻译成独立保存的目标程序。

解释器:

翻译源程序时不生成独立的目标程序解释程序和源程序要参与到程序的运行过程中

编译器:

翻译时将源程序翻译成独立保存的目标程序

机器上运行的是与源程序等价的目标程序:源程序和编译程序都不再参与目标程序的运行过程

程序设计语言的数据成分

数据名称由用户通过标识符命名,标识符是由字母、数字和下划线”_” 组成标记

1)常量和变量

按照程序运行时数据的值能否改变,将数据分为常量和变量。程序中的数据对象可以具有 左值和(或)右值,左值指存储单元(或地址、容器),右值是值(或内容)。变量具有左值和 右值,在程序运行过程中其右值可以改变;常量只有右值,在程序运行过程中其右值不能改变。

2)全局量和局部量

数据按在程序代码中的作用范围(作用域)可分为全局量和局部量。一般情况下,全局变 量的作用域为整个文件或程序,系统为全局变量分配的存储空间在程序运行的过程中是不改变

3)数据类型

按照数据组织形式的不同可将数据分为基本类型、用户定义类型、构造类型及其他类型。
C(C++)的数据类型如下。

  1. 基本类型:整型(int)、字符型(char)、实型(float、double)和布尔类型(bool)。
  2. 特殊类型:空类型(void)。
  3. 用户定义类型:枚举类型(enum)。
  4. 构造类型:数组、结构、联合。
  5. 指针类型:type*。
  6. 抽象数据类型:类类型。

其中,布尔类型和类类型由C+语言提供。

1顺序结构
顺序结构用来表示一个计算操作序列。计算过程从所描述的第一个操作开始,按顺序依次 执行后续的操作,直到序列的最后一个操作。在顺序结构内也可以包含其他控 制结构。
2)选择结构
选择结构提供了在两种或多种分支中选择其中一个的逻辑。基本的选择结构是指定一个条 件P,然后根据条件的成立与否决定控制流计算A还是计算B,从两个分支中选择一个执行, 如图2-2(a)所示。选择结构中的计算A或计算B还可以包含顺序、选择和重复结构。程序设 计语言中还通常提供简化了的选择结构,也就是没有计算B的分支结构,如图2-2(b)所示。
3)循环结构
循环结构描述了重复计算的过程,通常由三部分组成:初始化、循环体和循环条件,其中 初始化部分有时在控制的逻辑结构中不进行显式的表示。循环结构主要有两种形式:while型

base_64 image

计算机系统基础知识

计算机系统硬件基本储存

计算机系统是有软硬件组成

计算机的基本硬件系统是由:运算器、控制器、存储器、输入设备和输出设备5大部件组成。

运算器、控制器等部件被系统集成在一起统称为中央处理器单元(CPU)。CPU是硬件系统核心。

中央处理单元(CPU)

中央处理器单元(CPU)是计算机系统的核心部件,他负责获取程序指令、对指令进行译码并加以执行

CPU的功能

程序控制(控制器)

CPU通过执行指令来控制程序的执行顺序,这是CPU的重要功能

操作控制(控制器)

一条指令功能的实现需要若干操作型号配合来完成,CPU产生每条指令操作信号并将操作信号送往对应的部件,控制相应的部件按指令的功能进行操作

时间控制(运算器)

CPU对各种操作进行时间上的控制,即指令执行过程中操作信号的出现时间、持续时间及出现的时间顺序都需要进行严格控制。

数据处理(运算器)

CPU通过对数据进行算术运算及逻辑运算等方式进行加工处理,数据加工处理的结果被人们所利用。所以,对数据的加工处理也是CPU最根本的任务。

运算器

运算器由算术逻辑单元、累加寄存器、数据缓冲寄存器和状态条件寄存器组成。

运算器有两个主要功能:

  1. 执行所有的算术运算,例如加、减、乘、除等基本运算及附加运算。
  2. 执行所有的逻辑运算并进行逻辑测试,例如与、或、非、零值测试或两个值的比较等。

运算器中各组成部件的功能:

算术逻辑单元ALU:

ALU是运算器的重要组成部件,负责处理数据,实现对数据的算术运算和逻辑运算,

累加寄存器AC:

AC通常简称为累加器,他是一个通用寄存器,其功能当运算器的算术逻辑单元执行算术或逻辑运算时,为ALU提供一个工作区。

例如,在执行一个减法运算前,先将被减树取出暂存在AC中,再从内储存器中取出减数,然后同AC的内容相减,将所得的结果送回AC中,运算的结果是放在累加器中的,运算器中至少要有一个累加寄存器。

数据缓冲寄存器DR:

在对内存储器进行读/写操作时,用DR暂时存放由内存储存器读/写的一条指令或一个数据

DR的主要作用为:作为CPU和内存、外部设备之间数据传送的中转站;作为CPU和内存、外围设备之间在操作速度上的缓冲:在单累加器结构的运算器中,数据缓冲寄存器还可兼作为操作数寄存器

状态条件寄存器PSW:

PSW保存有算术指令和逻辑指令运行或测试的结果建立的各种条件码内容。

保存了当前指令执行完成之后的状态,通常,一个算术操作产生一个运算结果,而一个逻辑操作产生一个判决。

控制器

控制器用于控制整个CPU的工作,他决定了计算机运行过程的自动化。他不仅要保证程序的正确执行,而且还要能够处理异常事件。

指令控制逻辑要完成取指令、分析指令和执行指令的操作,其过程分为取指令、指令译码、按指令操作码执行、形成下一条指令地址等步骤

控制器中各组成部件的功能:

指令寄存器IR:

存放的是从内存中取得指令,就像个中间站一样,不过是存放指令的中间站

当CPU执行一条指令时,先把他从内存存储器取到缓冲寄存器中,在送入IR暂存,指令译码器根据IR的内容产生各种微操作指令控制其他的组成部件工作。

程序计数器PC:

存放的是指令的地址,还有计数的功能

PC具有寄存信息和计数两种功能,又称为指令计数器

在程序开始执行前,将程序的起始地址送入PC,该地址在程序加载到内存时确定,因此PC的内容即是程序第一条指令的地址。执行指令时,CPU自动修改PC的内容,以便使其保持的总是将要执行的下一条指令的地址。由于大多数指令都是按顺序来执行的,所以修改的过程通常只是简单地对PC加1。当遇到传移指令时,后继指令的地址根据当前指令的地址加上一个向前或向后转移的位移量得到,或者根据转移指令给出的直接转移的地址得到。

地址寄存器AR:

存放的是cpu访问内存单元的地址

AR保存当前CPU所访问的内存单元地址。

指令译码器ID:

是把操作码解析成对应的指令操作

指令包含操作码和地址两个部分,为了能执行任何给定的指令,必须对操作码进行缝隙,指令译码器就是对指令中的操作码字段进行缝隙解释,识别该指令规定的操作,向操作控制器发出具体的控制器信号,控制各部件工作,完成所需的功能。

计算机基本单位

单位名称 英文表述
(比特)*[最小数据单位]* b/bit
字节 [与b相差8倍] [最小存储单位] B/byte
千字节 [与B相差1024倍] KB
兆字节 [与KB相差1024倍] MB
吉字节 [与MB相差1024倍] GB
太字节 [与GB相差1024倍] TB

数据的表示

进制转换

R->10

R进制转十进制使用按权展开法,其具体的操作方式为:将R进制数每一位数值用
$$
R^k
$$
来表示,k与该为何小数点之间的距离有关,当该位位于小数点左边,k的值是该位数的小数点之间的数码的个数

当该位数位于小数点右边,k值是负值,其绝对值是该位和小数点之间码数的个数+1

*例如 二进制:10100.01 = 1*2^4 + 1*2^2 + 1*2^-2

*例如 七进制:604.01 = 6*7^2 + 4*7^0 + 1*7^-2

10->R

十进制转R进制使用短除法

例如将94转换为2进制数*(取余数(除到最后的数小于进制))

94 47 23 11 5 2 1
除进制 2 2 2 2 2 2
余数 0 1 1 1 1 0 1

然后反取得:1011110

2->8

$$
2^3 = 8
$$

*举例 10001110 转8进制

拆分为

10 001 110
2^1 2^0 2^2+2^1
2 1 6

所以得出结果为216
$$
O216 或
(216)_8
$$

2->16

$$
2^4 = 16
$$

*举例 1001110 转16进制

拆分为

1000 1110
2^3 2^3+2^2+2^1
8 8+4+2=14 (A,B,C,D,E)
8 E

所以转换为16进制为8E
$$
( 8E)_{16}或OX8E或8EH
$$

进制得加减法

加法遇n进一,减法借1当n

原码,反码,补码,移码

机器自查为8位2进制数,第一位为符号位:0表示正数,1表示负数 ;后七位表示数

数值1 数值-1 1-1【1+(-1)】
原码 0000 0001 1000 0001 1000 0010(左边两个相加)
反码 0000 0001(一样) 1111 1110 (除符号位,其他全变) 1111 1111(左边两个相加)
补码 0000 0001(一样) 1111 1111(在反码的基础上+1) 0000 0000(左边两个相加)
移码 1000 0001(在补码基础上,变化符号位) 0111 1111(补发基础上,变化符号位) 1000 0000

**只有补码能加减计算

数据表示范围

补码和移码都没有-0

image-20220918101942613.png image-20220918101357424.png

浮点数

$$
N= 尾数*基数^{指数}
$$

$$
举例:3.14*10^3
$$

其中的

3代表的就是阶码

特点

  1. 一般尾数用补码,阶码用移码
  2. 阶码的位数决定数的表示范围,位数越多表示范围越大
  3. 尾数的位数决定数的有效精度,位数越多精度越高
  4. 对阶时,小数向大数看齐
  5. 对阶是通过比较小数的尾数右移实现的

运算过程

对阶》尾数计算》结果格式化

存储格式

3.14*10^3

阶符 阶码 数符 尾数
0(阶码为正:0) 0(数字的正负)

浮点数。当机器字长为n时,定点数的补码和移码可表示2个数,而其原码和反码只 能表示2”-1个数(0的表示占用了两个编码),因此,定点数所能表示的数值范围比较小,在 运算中很容易因结果超出范围而溢出。浮点数是小数点位置不固定的数,它能表示更大范围 的数。

Flynn计算机体系结构分类

体系结构类型 结构 代表
单指令流单数据SISD 控制部分:1
处理器:1
主存模块:1
单处理器系统
单指令流多数据流SIMD 控制部分:1
处理器:N
主存模块:N
并行处理机
阵列处理机
超级向量处理机
多指令流单数据流MISD 控制部分:N
处理器:1
主存模块:N
目前没有,有理论流水线计算机
多指令流多数据流MIMD 控制部分:N
处理器:N
主存模块:N
多处理机系统
多计算机

指令的基本概念

一条指令就是机器语言的一个语句,它是一组有意义的二进制代码,指令的基本格式如下:

例如a+b=c

操作码字段 地址码字段 地址码字段 地址码字段
+ a b c
操作码字段 地址码字段 地址码字段
+ a b

例如自增就只需一个地址码字段

操作码部分指出了计算机要执行什么性质的操作,如加法、减法、取数、存数等。地址码字段需要包含各操作数的地址及操作结果的存放地址等,从其地址结构的角度可以分为三地址指令、二地址指令、一地址指令和零地址指令。

寻址方式

名称 特点
立即寻址方式 操作数直接在指令中,速度快,灵活性差
直接寻址方式 指令中存放的是操作数的地址
间接寻址放肆 指令中存放了一个地址,这个地址对应的内容是操作数的地址
寄存器寻址方式 寄存器存放操作数
寄存器间接寻址方式 寄存器内存放的是操作数的地址
image3519aa493e77cd08.png

CISC与RISC

指令系统类型 指令 寻址方式 实现方式 其他
CISC(复杂) 数量多,使用频率差别大,可变长格式 支持多种 微程序控制技术(微码) 研制周期长
RISC(精简) 数量少,使用频率接近,定长格式,大部分为单周期指令,操作寄存器,只有Load/Store操作内存 支持方式少 增加了通用寄存器;硬布线逻辑控制为主适合采用流水线 优化编译,有效支持高级语言

CISC:复杂,指令数量多,频率差别大,多寻址
RISC:精简,指令数量少,操作奇存器,单周期,少寻址,多通用寄存器,流水线

流水线

概念

流水线是指在程序执行时多条指令重量进行操作的一种准并行处理实现技术。各种部件同时处理是针对不同指令而言的,它们可同时为多条指令的不同部分进行工作,以提高各部件的利用率和指令的平均执行速度

imagea442034d665d11fb.png

流水线计算公式:

流水线周期为执行最长的一段
$$
1条指令执行时间+(指令条数-1)*流水线周期
$$

  1. 理论公式
    $$
    (t1+t2+…+tk)+(n-1)*Δt
    $$

  2. 实践公式(理论公式没答案,使用实现公式)
    $$
    k*Δt+(n-1)*Δt
    $$

    image3eed5a15a68a300b.png

超标量流水线

imagead3f86bc2e51fe00.png

多了一个概念,度,本图度为2

吞吐率计算

流水线的吞吐率(Though Put rate,TP)是指在单位时间内流水线所完成的任务数量或输出的结果数量,计算流水线吞吐率的最基本的公式如下:
$$
TP=\frac{指令条数}{流水线执行时间}
$$
流水线最大吞吐率:
$$
TP_{max}=\lim_{n\to\infty}\frac{n}{(k+n-1)\Delta t}=\frac{1}{\Delta t}
$$

存储结构

名称 描述 速度(容量)
CPU 寄存器 最快(容量最小)成本高
Cache 按内容存取 快(容量较小)
内存(主存) 随机存储器(RAM)
只读存储器(ROM)
较慢(容量大)
外存(辅存) 硬盘,光盘,U盘等 最慢(容量最大)

Cache

在计算机的存储系统体系中,Cache是访问速度最快的层次(若有寄存器,则寄存器最快)。

使用Cache改善系统性能的依据是程序的局部性原理:

  1. 时间局部性:前后续访问不会被淘汰
  2. 空间局部性:相邻空间可以被范围

如果以h代表对Cache的访问命中率,t1表示Cache的周期时间,t表示主存储器周期时间,以读操作为例,使用“Cache+主存储器”的系统的平均周期为t3,则:
$$
t_3 = h*t_1+(1-h)*t_2
$$
其中,(1-h)又称为失效率(未命中率)。

映像

  1. 直接相邻映像:硬件电路较简单,但冲突率高

    (0页只能放在0页中,重复的旧页将会被淘汰,所以冲突率高)imagefcfa87893df69421.png

  2. 全相联映像:电路难于设计和实现,只适合于小容量的cache,冲突率较低(cache里面的每一页,都会存储主存里的每一页)

    imagebe633d0f7729b834.png
  3. 组相联映像:直接相联与全相联的折中

    (先分区后分组,0组放0组,组内随便放,对管理系统的消耗很大)

    imagee1a88ccf0c96841b.png

地址映像是将主存与Cache的存储空间划分为若干大小相同的页(称为块)。
例如,某机的主存容量为1GB,划分为2048页,每页512KB;Cache容量为8MB,划分为16页,每页512KB。

编址与计算

存储单元
按字编址:存储体的存储单元是字存储单元,即最小寻址单位是一个字
按字节编址:存储体的存储单元是字节存储单元;即最小寻址单位是一个字节(Byte=8位bit)。

记住公式:
$$
总片数=总容量/每片的容量
$$
例:若内存地址区间为4000H~43FFH,每个存储单元可存储16位二进制数,该内存区域用4片存储器芯片构成,则构成该内存所用的存储器芯片的容量是多少?

1
2
3
4
5
43FFH-4000H+1=4400H-4000H=400H
一位16进制 对应4位2进制 所以00H位8位
4化为2进制0100 得出2位
8+2=10位
(2^10*16(bit)位二进制数)/4片存储器=256*16bit

总线

一条总线同一时刻仅允许一个设备发送,但允许多个设备接收。
总线的分类:

  1. 数据总线(Data Bus):在CPU与RAM之间来回传送需要处理或是需要储存的数据。
  2. 地址总线(Address Bus):用来指定在RAM(Random Access Memory)之中储存的数据的地址。
  3. 控制总线(ControlBus):将微处理器控制单元(ControlUnit)的信号,传送到周边设备,一般常见的为USB Bus和1394 Bus。

串联系统和并联系统

一般求可靠性

串联:可靠性*可靠性……

并联:1-(1-可靠性)*(1-可靠性)……

imagec9e992857cb375a7.png

N模混合系统

imagea6b7eb3dbebb0dec.png

校验码

码距:任何一种编码都由许多码字构成,任意两个码字之间最少变化的二进制位数就称为数据校验码的码距。
例如,用4位二进制表示16种状态,则有16个不同的码字,此时码距为1。如0000与0001。(换几个码距就是几个)

奇偶校验

奇偶校验码的编码方法是:由若干位有效信息(如一个字节),再加上一个二进制位(校验位)组成校验码。

  1. 奇校验:整个校验码(有效信息位和校验位)中“1”的个数为奇数。

  2. 偶校验:整个校验码(有效信息位和校验位)中“1”的个数为偶数。

奇偶校验,可检查1位的错误,不可纠错。

例如:1010

校验码【1010(1)】 的1就为奇数

如果这是传输后的校验码为【1011(1)】,即1为偶数了,就发现数据发生错误,但如果两位发送错误,如【1111(1)】那么就检测不出来了。

循环冗余校验码CRC

CRC校验,可检错,不可纠错。
CRC的编码方法是:在k位信息码之后拼接r位校验码。应用CRC码的关键是如何从k位信息位简便地得到r位校验位(编码),以及如何从k+r位信息码判断是否出错。
循环穴余校验码编码规律如下:

  1. 把待编码的N位有效信息表示为多项式M(X);

  2. 把M(X)左移K位,得到M(X)×XK,这样空出了K位,以便拼装K位余数(即校验位);

  3. 选取一个K+1位的产生多项式G(X),对M(X)×X做模2除;

  4. 把左移K位以后的有效信息与余数R(X)做模2加减,拼接为CRC码,此时的CRC码共有N+K位。

把接收到的CRC码用约定的生成多项式G(X)去除,如果正确,则余数为0;如果某一位出错,则余数不为0。不同的位数出错其余数不同,余数和出错位序号之间有惟一的对应关系。

image5e822a50ee2e56f6.png

这个的意思就是指,先除两个数,然后将余数补在原数后面发送,数据除不尽即说明数据发生了错误

image9f1866ff2748ae1f.png

海明校验码

海明校验,可检错,也可纠错。
海明校验码的原理是:在有效信息位中加入几个校验位形成海明码使码距比较均匀地拉大,并把海明码的每个二进制位分配到几个奇偶校验组中,当某一位出错后,就会引起有关的几个校验位的值发生变化,这不但可以发现错误,还能指出错误的位置,为自动纠错提供了依据
$$
2^r\geq m+r+1
$$

计算机基础知识例题

例题

进制转换题

①.2015年下

image-20220918151931192.png

②.2017年下

image-20220918154932132.png

答案

进制转换题

①.2015年下

DABFFH-B3000+1

加一得原因就是因为存储容量必须包含B3000H这个数

根据后面得H可知该数为16进制数

被减数 D A B F F
减数 B 3 0 0 0
2 7 B F F
再加+1 2 7 C 0 0

因为选项中没有带H,所以需要将数转为十进制数。

2 7 C 0 0
2*16^4 7*16^3 12*16^2 0*16^1 0*16^0

其中的2*16^4也可以化简为:
$$
16^4=(2^4)^4=2^{16}
$$
因为我们得出来得数的单位为最小存储单位B,所以需要将最后的结果/1024,也就是除以:
$$
2^{10}
$$
以此类推,得

2*16^4 7*16^3 12*16^2 0*16^1 0*16^0
化简 2*2^16 7*2^12 12*2^8 0 0
除以2^10 2*2^6 7*2^2 12*2^-2 0 0

相加得:
$$
22^6+72^2+122^{-2}=264+74+12\frac{1}{4}=128+28+3=159(KB)
$$
所以结果就是B:159KB

②.2017年下

因为2015年下已经表述很清楚了,后面就简写

DFFFF+1-A0000,E0000-A0000=40000

转为10(D)进制数 4*16^4

好,接下来就是重点了

因为内存按字节编地址(所以我们求出来的数的单位为B/Btye),用存储容量为32K*8bit的存储器芯片构成地址:

所以可以得出:
$$
x32K8bit=416^4Btye
$$
因为*8Btye=8bit,1K=1024B=2^10B

转换单位为:
$$
x32K8bit=\frac{4*2^{16}}{2^{10}}8bit=256K8bit
$$

$$
x=\frac{256K8bit}{32K8bit}
$$

求x为8,所以答案为B:8

程序设计语言

低级语言和高级语言

计算机硬件只能识别由0、1组成的机器指令序列,即机器指令程序,因此机器指令是最基本的计算机语言。

例如,用ADD表示加法、用SUB表示减法等。用符号表示的指令称为汇编指令,汇编指令的集合被称为汇编语言。

人们称机器语言和汇编语言为低级语言。

面向各类应用的程序程序设计语言为高级语言。(JAVA、C、C++、PHP、Python、Delphi、PASCAL等)。

编译程序和解释程序

语言之间的翻译形式有多种,基本方式为汇编、解释和编译

用某种高级语言或汇编语言编写的程序称为源程序,源程序不能直接在计算机上执行。如果源程序是用汇编语言编写的,则需要一个汇编程序将其翻译成目标程序后才能执行。如果源程序是用某种高级语言编写的,则需要对应的解释程序或编译程序对其进行翻译,然后在机器上运行。

解释程序也称为解释器,它或者直接解释执行源程序,或者将源程序翻译成某种中间代码后再加以执行;

  • 而编译程序(编译器)则是将源程序翻译成目标语言程序,然后在计算机上运行目标程序。这两种语言处理程序的根本区别是:
  • 而在解释方式下,解释程序和源程序(或其某种等价表示)要参与到程序的运行过程中,运行程序的控制权在解释程序。

简单来说,在解释方式下,翻译源程序时不生成独立的目标程序,而编译器则将源程序翻译成独立保存的目标程序。

解释器:

翻译源程序时不生成独立的目标程序解释程序和源程序要参与到程序的运行过程中

编译器:

翻译时将源程序翻译成独立保存的目标程序

机器上运行的是与源程序等价的目标程序:源程序和编译程序都不再参与目标程序的运行过程

程序设计语言的数据成分

数据名称由用户通过标识符命名,标识符是由字母、数字和下划线”_” 组成标记

1)常量和变量

按照程序运行时数据的值能否改变,将数据分为常量和变量。程序中的数据对象可以具有 左值和(或)右值,左值指存储单元(或地址、容器),右值是值(或内容)。变量具有左值和 右值,在程序运行过程中其右值可以改变;常量只有右值,在程序运行过程中其右值不能改变。

2)全局量和局部量

数据按在程序代码中的作用范围(作用域)可分为全局量和局部量。一般情况下,全局变 量的作用域为整个文件或程序,系统为全局变量分配的存储空间在程序运行的过程中是不改变

3)数据类型

按照数据组织形式的不同可将数据分为基本类型、用户定义类型、构造类型及其他类型。
C(C++)的数据类型如下。

  1. 基本类型:整型(int)、字符型(char)、实型(float、double)和布尔类型(bool)。
  2. 特殊类型:空类型(void)。
  3. 用户定义类型:枚举类型(enum)。
  4. 构造类型:数组、结构、联合。
  5. 指针类型:type*。
  6. 抽象数据类型:类类型。

其中,布尔类型和类类型由C+语言提供。

1顺序结构
顺序结构用来表示一个计算操作序列。计算过程从所描述的第一个操作开始,按顺序依次 执行后续的操作,直到序列的最后一个操作。在顺序结构内也可以包含其他控 制结构。
2)选择结构
选择结构提供了在两种或多种分支中选择其中一个的逻辑。基本的选择结构是指定一个条 件P,然后根据条件的成立与否决定控制流计算A还是计算B,从两个分支中选择一个执行, 如图2-2(a)所示。选择结构中的计算A或计算B还可以包含顺序、选择和重复结构。程序设 计语言中还通常提供简化了的选择结构,也就是没有计算B的分支结构,如图2-2(b)所示。
3)循环结构
循环结构描述了重复计算的过程,通常由三部分组成:初始化、循环体和循环条件,其中 初始化部分有时在控制的逻辑结构中不进行显式的表示。循环结构主要有两种形式:while型

Go的学习日记(4)- Go的语言函数

本文章基于C语言中文网学习整理而来

http://c.biancheng.net/golang/func/

声明(函数定义)

因为Go语言是编译型语言,所以函数编写的顺序是无关紧要的,鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。

编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务来解决,而且,同一个任务(函数)可以被多次调用,有助于代码重用(事实上,好的程序是非常注意 DRY 原则的,即不要重复你自己(Don’t Repeat Yourself),意思是执行特定任务的代码只能在程序里面出现一次)。

Go语言里面拥三种类型的函数:

  • 普通的带有名字的函数
  • 匿名函数或者 lambda 函数
  • 方法

普通函数声明(定义)

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

1
2
3
func 函数名(形式参数列表)(返回值列表){
函数体
}

形式参数列表描述了函数的参数名以及参数类型,这些参数作为局部变量,其值由参数调用者提供,返回值列表描述了函数返回值的变量名以及类型,如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。

1
2
3
4
func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3,4)) // "5"

空白标识符_可以强调某个参数未被使用。

1
func first(x int, _ int) int { return x }

函数的返回值

Go语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数,Go语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误,示例代码如下:

1
conn, err := connectToNetwork()

在这段代码中,connectToNetwork 返回两个参数,conn 表示连接对象,err 返回错误信息。

同一种类型的放回值

如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。

使用 return 语句返回时,值列表的顺序需要与函数声明的返回值类型一致,示例代码如下:

1
2
3
4
5
6
7
8
9
func typedTwoValues() (int, int) {
return 1, 2
}
func main() {
a, b := typedTwoValues()
fmt.Println(a, b)
}

//1 2

带有变量名的返回值

命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。

下面代码中的函数拥有两个整型返回值,函数声明时将返回值命名为 a 和 b,因此可以在函数体中直接对函数返回值进行赋值,在命名的返回值方式的函数体中,在函数结束前需要显式地使用 return 语句进行返回,代码如下:

1
2
3
4
5
6
7
8
9
10
func namedRetValues() (a, b int) {
a = 1
b = 2
return
}

func namedRetValues() (a, b int) {
a = 1
return a, 2
}

软件设计师知识点积累(1)

数据的表示

进制转换

R->10

R进制转十进制使用按权展开法,其具体的操作方式为:将R进制数每一位数值用
$$
R^k
$$
来表示,k与该为何小数点之间的距离有关,当该位位于小数点左边,k的值是该位数的小数点之间的数码的个数

当该位数位于小数点右边,k值是负值,其绝对值是该位和小数点之间码数的个数+1

*例如 二进制:10100.01 = 1*2^4 + 1*2^2 + 1*2^-2

*例如 七进制:604.01 = 6*7^2 + 4*7^0 + 1*7^-2

10->R

十进制转R进制使用短除法

例如将94转换为2进制数*(取余数(除到最后的数小于进制))

94 47 23 11 5 2 1
除进制 2 2 2 2 2 2
余数 0 1 1 1 1 0 1

然后反取得:1011110

2->8

$$
2^3 = 8
$$

*举例 10001110 转8进制

拆分为

10 001 110
2^1 2^0 2^2+2^1
2 1 6

所以得出结果为216
$$
O216 或
(216)_8
$$

2->16

$$
2^4 = 16
$$

*举例 1001110 转16进制

拆分为

1000 1110
2^3 2^3+2^2+2^1
8 8+4+2=14 (A,B,C,D,E)
8 E

所以转换为16进制为8E
$$
( 8E)_{16}或OX8E或8EH
$$

原码,反码,补码,移码

机器自查为8位2进制数,第一位为符号位:0表示正数,1表示负数 ;后七位表示数

数值1 数值-1 1-1【1+(-1)】
原码 0000 0001 1000 0001 1000 0010(左边两个相加)
反码 0000 0001(一样) 1111 1110 (除符号位,其他全变) 1111 1111(左边两个相加)
补码 0000 0001(一样) 1111 1111(在反码的基础上+1) 0000 0000(左边两个相加)
移码 1000 0001(在补码基础上,变化符号位) 0111 1111(补发基础上,变化符号位) 1000 0000

**只有补码能加减计算

原码:
$$
-(2^{n-1}-1)——+
$$

Go的学习日记(3)- Go的流体控制

本文章基于C语言中文网学习整理而来

http://c.biancheng.net/golang/flow_control/

if else(分支结构)

1
2
3
4
5
6
7
if condition1 {
// do something
} else if condition2 {
// do something else
}else {
// catch-all or default
}

else if 分支的数量是没有限制的,但是为了代码的可读性,还是不要在 if 后面加入太多的 else if 结构,如果必须使用这种形式,则尽可能把先满足的条件放在前面。

关键字 if 和 else 之后的左大括号{必须和关键字在同一行,如果你使用了 else if 结构,则前段代码块的右大括号}必须和 else if 关键字在同一行,这两条规则都是被编译器强制规定的。

1
2
3
4
if x{
}
else { // 无效的
}

要注意的是,在使用 gofmt 格式化代码之后,每个分支内的代码都会缩进 4 个或 8 个空格,或者是 1 个 tab,并且右大括号}与对应的 if 关键字垂直对齐。

在有些情况下,条件语句两侧的括号是可以被省略的,当条件比较复杂时,则可以使用括号让代码更易读,在使用 &&、|| 或 ! 时可以使用括号来提升某个表达式的运算优先级,并提高代码的可读性。

特殊写法

if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句

1
2
3
4
if err := Connect(); err != nil {
fmt.Println(err)
return
}

Connect 是一个带有返回值的函数,err:=Connect() 是一个语句,执行 Connect 后,将错误保存到 err 变量中。

err != nil 才是 if 的判断表达式,当 err 不为空时,打印错误并返回。

这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在 if、else 语句组合中。

提示

在编程中,变量的作用范围越小,所造成的问题可能性越小,每一个变量代表一个状态,有状态的地方,状态就会被修改,函数的局部变量只会影响一个函数的执行,但全局变量可能会影响所有代码的执行状态,因此限制变量的作用范围对代码的稳定性有很大的帮助。

for(循环结构)

与多数语言不同的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构,关键字 for 的基本使用方法与C语言和 C++ 中非常接近:

1
2
3
4
sum := 0
for i := 0; i < 10; i++ {
sum += i
}

可以看到比较大的一个不同在于 for 后面的条件表达式不需要用圆括号()括起来,Go语言还进一步考虑到无限循环的场景,让开发者不用写无聊的 for(;;){}do{} while(1);,而直接简化为如下的写法:

1
2
3
4
5
6
7
sum := 0
for {
sum++
if sum > 100 {
break
}
}

使用循环语句时,需要注意的有以下几点:

  • 左花括号{必须与 for 处于同一行。
  • Go语言中的 for 循环与C语言一样,都允许在循环条件中定义和初始化变量,唯一的区别是,Go语言不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量。
  • Go语言的 for 循环同样支持 continue 和 break 来控制循环,但是它提供了一个更高级的 break,可以选择中断哪一个循环,如下例:
1
2
3
4
5
6
7
8
9
10
for j := 0; j < 5; j++ {
for i := 0; i < 10; i++ {
if i > 5 {
break JLoop
}
fmt.Println(i)
}
}
JLoop:
// ...

上述代码中,break 语句终止的是 JLoop 标签处的外层循环。

for 中的初始语句——开始循环时执行的语句

初始语句是在第一次循环前执行的语句,一般使用初始语句执行变量初始化,如果变量在此处被声明,其作用域将被局限在这个 for 的范围内。

初始语句可以被忽略,但是初始语句之后的分号必须要写,代码如下:

1
2
3
4
step := 2
for ; step > 0; step-- {
fmt.Println(step)
}

这段代码将 step 放在 for 的前面进行初始化,for 中没有初始语句,此时 step 的作用域就比在初始语句中声明 step 要大。

for 中的条件表达式——控制是否循环的开关

每次循环开始前都会计算条件表达式,如果表达式为 true,则循环继续,否则结束循环,条件表达式可以被忽略,忽略条件表达式后默认形成无限循环。

1) 结束循环时带可执行语句的无限循环

下面代码忽略条件表达式,但是保留结束语句,代码如下:

1
2
3
4
5
6
var i int
for ; ; i++ {
if i > 10 {
break
}
}

代码说明如下:

  • for ; ; i++ ,无须设置 i 的初始值,因此忽略 for 的初始语句,两个分号之间是条件表达式,也被忽略,此时循环会一直持续下去,for 的结束语句为 i++,每次结束循环前都会调用。
  • if i > 10 ,判断 i 大于 10 时,通过 break 语句跳出 for 循环到第 9 行。

2) 无限循环

上面的代码还可以改写为更美观的写法,代码如下:

1
2
3
4
5
6
7
var i int
for {
if i > 10 {
break
}
i++
}

代码说明如下:

  • for {,忽略 for 的所有语句,此时 for 执行无限循环。
  • i++,将 i++ 从 for 的结束语句放置到函数体的末尾是等效的,这样编写的代码更具有可读性。

无限循环在收发处理中较为常见,但需要无限循环有可控的退出方式来结束循环。

3) 只有一个循环条件的循环

在上面代码的基础上进一步简化代码,将 if 判断整合到 for 中,变为下面的代码:

1
2
3
4
var i int
for i <= 10 {
i++
}

for i <= 10,将之前使用if i>10{}判断的表达式进行取反,变为判断 i 小于等于 10 时持续进行循环。

上面这段代码其实类似于其他编程语言中的 while,在 while 后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。

for 中的结束语句——每次循环结束时执行的语句

在结束每次循环前执行的语句,如果循环被 break、goto、return、panic 等语句强制退出,结束语句不会被执行。

for range(键值循环)

for range 结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道(channel),for range 语法上类似于其它语言中的 foreach 语句,一般形式为:

1
2
3
for key, val := range coll {
...
}

需要要注意的是,val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值。一个字符串是 Unicode 编码的字符(或称之为 rune )集合,因此也可以用它来迭代字符串:

1
2
3
for pos, char := range str {
...
}

每个 rune 字符和索引在 for range 循环中是一一对应的,它能够自动根据 UTF-8 规则识别 Unicode 编码的字符。

通过 for range 遍历的返回值有一定的规律:

  • 数组、切片、字符串返回索引和值。
  • map 返回键和值。
  • 通道(channel)只返回通道内的值。

遍历数组、切片——获得索引和值

在遍历代码中,key 和 value 分别代表切片的下标及下标对应的值,下面的代码展示如何遍历切片,数组也是类似的遍历方法:

1
2
3
for key, value := range []int{1, 2, 3, 4} {
fmt.Printf("key:%d value:%d\n", key, value)
}

遍历字符串——获得字符

Go语言和其他语言类似,可以通过 for range 的组合,对字符串进行遍历,遍历时,key 和 value 分别代表字符串的索引和字符串中的每一个字符。

下面这段代码展示了如何遍历字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var str = "hello 你好"
for key, value := range str {
fmt.Printf("key:%d value:0x%x\n", key, value)
}
/*
key:0 value:0x68
key:1 value:0x65
key:2 value:0x6c
key:3 value:0x6c
key:4 value:0x6f
key:5 value:0x20
key:6 value:0x4f60
key:9 value:0x597d
*/

代码中的变量 value,实际类型是 rune 类型,以十六进制打印出来就是字符的编码。

遍历 map——获得 map 的键和值

对于 map 类型来说,for range 遍历时,key 和 value 分别代表 map 的索引键 key 和索引对应的值,一般被称为 map 的键值对,因为它们是一对一对出现的,下面的代码演示了如何遍历 map。

1
2
3
4
5
6
7
m := map[string]int{
"hello": 100,
"world": 200,
}
for key, value := range m {
fmt.Println(key, value)
}

对 map 遍历时,遍历输出的键值是无序的,如果需要有序的键值对输出,需要对结果进行排序。

遍历通道(channel)——接收通道数据

for range 可以遍历通道(channel),但是通道在遍历时,只输出一个值,即管道内的类型对应的数据。

1
2
3
4
5
6
7
8
9
10
c := make(chan int)
go func() {
c <- 1
c <- 2
c <- 3
close(c)
}()
for v := range c {
fmt.Println(v)
}

代码说明如下:

  • 第 1 行创建了一个整型类型的通道。
  • 第 3 行启动了一个 goroutine,其逻辑的实现体现在第 5~8 行,实现功能是往通道中推送数据 1、2、3,然后结束并关闭通道。
  • 这段 goroutine 在声明结束后,在第 9 行马上被执行。
  • 从第 11 行开始,使用 for range 对通道 c 进行遍历,其实就是不断地从通道中取数据,直到通道被关闭。

在遍历中选择希望获得的变量

在使用 for range 循环遍历某个对象时,一般不会同时需要 key 或者 value,这个时候可以采用一些技巧,让代码变得更简单,下面将前面的例子修改一下,参考下面的代码示例:

1
2
3
4
5
6
7
m := map[string]int{
"hello": 100,
"world": 200,
}
for _, value := range m {
fmt.Println(value)
}

在上面的例子中将 key 变成了下划线_,这里的下划线就是匿名变量。

  • 可以理解为一种占位符。
  • 匿名变量本身不会进行空间分配,也不会占用一个变量的名字。
  • 在 for range 可以对 key 使用匿名变量,也可以对 value 使用匿名变量。

再看一个匿名变量的例子:

1
2
3
for key, _ := range []int{1, 2, 3, 4} {
fmt.Printf("key:%d \n", key)
}

在该例子中,value 被设置为匿名变量,只使用 key,而 key 本身就是切片的索引,所以例子输出索引。

总结

我们总结一下 for 的功能:

  • Go语言的 for 包含初始化语句、条件表达式、结束语句,这 3 个部分均可缺省。
  • for range 支持对数组、切片、字符串、map、通道进行遍历操作。
  • 在需要时,可以使用匿名变量对 for range 的变量进行选取。

switch case语句

Go语言的 switch 要比C语言的更加通用,表达式不需要为常量,甚至不需要为整数,case 按照从上到下的顺序进行求值,直到找到匹配的项,如果 switch 没有表达式,则对 true 进行匹配,因此,可以将 if else-if else 改写成一个 switch。

基本写法

Go语言改进了 switch 的语法设计,case 与 case 之间是独立的代码块,不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = "hello"
switch a {
case "hello":
fmt.Println(1)
case "world":
fmt.Println(2)
default:
fmt.Println(0)
}

/*
1
*/

上面例子中,每一个 case 均是字符串格式,且使用了 default 分支,Go语言规定每个 switch 只能有一个 default 分支。

1) 一分支多值

当出现多个 case 要放在一起的时候,可以写成下面这样:

1
2
3
4
5
var a = "mum"
switch a {
case "mum", "daddy":
fmt.Println("family")
}

不同的 case 表达式使用逗号分隔。

2) 分支表达式

case 后不仅仅只是常量,还可以和 if 一样添加表达式,代码如下:

1
2
3
4
5
var r int = 11
switch {
case r > 10 && r < 20:
fmt.Println(r)
}

注意,这种情况的 switch 后面不再需要跟判断变量。

跨越 case 的 fallthrough——兼容C语言的 case 设计

在Go语言中 case 是一个独立的代码块,执行完毕后不会像C语言那样紧接着执行下一个 case,但是为了兼容一些移植代码,依然加入了 fallthrough 关键字来实现这一功能,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
var s = "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough
case s != "world":
fmt.Println("world")
}
/*
hello
world
*/

新编写的代码,不建议使用 fallthrough。

goto语句——跳转到指定的标签

Go语言中 goto 语句通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助,使用 goto 语句能简化一些代码的实现过程。

使用 goto 退出多层循环

下面这段代码在满足条件时,需要连续退出两层循环,使用传统的编码方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"
func main() {
var breakAgain bool
// 外循环
for x := 0; x < 10; x++ {
// 内循环
for y := 0; y < 10; y++ {
// 满足某个条件时, 退出循环
if y == 2 {
// 设置退出标记
breakAgain = true
// 退出本次循环
break
}
}
// 根据标记, 还需要退出一次循环
if breakAgain {
break
}
}
fmt.Println("done")
}

代码说明如下:

  • for x := 0; x < 10; x++ ,构建外循环。
  • for y := 0; y < 10; y++,构建内循环。
  • if y == 2 ,当 y==2 时需要退出所有的 for 循环。
  • breakAgain = true,默认情况下循环只能一层一层退出,为此就需要设置一个状态变量 breakAgain,需要退出时,设置这个变量为 true。
  • break,使用 break 退出当前循环,执行后,代码调转到第 28 行。
  • if breakAgain,退出一层循环后,根据 breakAgain 变量判断是否需要再次退出外层循环。
  • fmt.Println("done"),退出所有循环后,打印 done。

将上面的代码使用Go语言的 goto 语句进行优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
func main() {
for x := 0; x < 10; x++ {
for y := 0; y < 10; y++ {
if y == 2 {
// 跳转到标签
goto breakHere
}
}
}
// 手动返回, 避免执行进入标签
return
// 标签
breakHere:
fmt.Println("done")
}

代码说明如下:

  • goto breakHere,使用 goto 语句跳转到指明的标签处,标签在第 23 行定义。
  • return,标签只能被 goto 使用,但不影响代码执行流程,此处如果不手动返回,在不满足条件时,也会执行第 24 行代码。
  • breakHere:,定义 breakHere 标签。

使用 goto 语句后,无须额外的变量就可以快速退出所有的循环。

使用 goto 集中处理错误

多处错误处理存在代码重复时是非常棘手的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
err := firstCheckError()
if err != nil {
fmt.Println(err)
exitProcess()
return
}
err = secondCheckError()
if err != nil {
fmt.Println(err)
exitProcess()
return
}
fmt.Println("done")

代码说明如下:

  • err := firstCheckError(),执行某逻辑,返回错误。
  • if err != nil{} ,如果发生错误,打印错误退出进程。
  • err = secondCheckError(),执行某逻辑,返回错误。
  • if err != nil {},发生错误后退出流程。

在上面代码中,有一部分都是重复的错误处理代码,如果后期在这些代码中添加更多的判断,就需要在这些雷同的代码中依次修改,极易造成疏忽和错误。

使用 goto 语句来实现同样的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
    err := firstCheckError()
if err != nil {
goto onExit
}
err = secondCheckError()
if err != nil {
goto onExit
}
fmt.Println("done")
return
onExit:
fmt.Println(err)
exitProcess()

代码说明如下:

  • goto onExit,发生错误时,跳转错误标签 onExit。
  • fmt.Println(err) exitProcess(),汇总所有流程进行错误打印并退出进程。

break(跳出循环)

Go语言中 break 语句可以结束 for、switch 和 select 的代码块,另外 break 语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的 for、switch 和 select 的代码块上。

跳出指定循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
func main() {
OuterLoop:
for i := 0; i < 2; i++ {
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i, j)
break OuterLoop
case 3:
fmt.Println(i, j)
break OuterLoop
}
}
}
}
/*
0 2
*/

代码说明如下:

  • OuterLoop:,外层循环的标签。
  • for i := 0; i < 2; i++ for j := 0; j < 5; j++,双层循环。
  • switch j {,使用 switch 进行数值分支判断。
  • break OuterLoop,退出 OuterLoop 对应的循环之外,也就是跳转到第 20 行。

continue(继续下一次循环)

Go语言中 continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加标签时,表示开始标签对应的循环,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"
func main() {
OuterLoop:
for i := 0; i < 2; i++ {
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i, j)
continue OuterLoop
}
}
}
}

/*
0 2
1 2
*/

代码说明:第 14 行将结束当前循环,开启下一次的外层循环,而不是第 10 行的循环。

Go的学习日记(2)- Go的语言容器

本文章基于C语言中文网学习整理而来

http://c.biancheng.net/golang/container/

数组

数组声明

1
2
3
4
5
var 变量名 [元素数量]Type

var a [3]int // 定义三个整数的数组
fmt.Println(a[0]) // 打印第一个元素
fmt.Println(a[len(a)-1]) // 打印最后一个元素

语法说明如下所示:

  • 数组变量名:数组声明及使用时的变量名。
  • 元素数量:数组的元素数量,可以是一个表达式,但最终通过编译期计算的结果必须是整型数值,元素数量不能含有到运行时才能确认大小的数值。
  • Type:可以是任意基本类型,包括数组本身,类型为数组本身时,可以实现多维数组。
1
2
3
4
5
6
7
8
9
10
// 打印索引和元素
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}

// 仅打印元素
for _, v := range a {
fmt.Printf("%d\n", v)
}

默认情况下,数组的每个元素都会被初始化为元素类型对应的零值,对于数字类型来说就是 0,同时也可以使用数组字面值语法,用一组值来初始化数组:

1
2
3
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
fmt.Println(r[2]) // "0

在数组的定义中,如果在数组长度的位置出现“…”省略号,则表示数组的长度是根据初始化值的个数来计算,因此,上面数组 q 的定义可以简化为:

1
2
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

数组的长度是数组类型的一个组成部分,因此 [3]int 和 [4]int 是两种不同的数组类型,数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

1
2
q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // 编译错误:无法将 [4]int 赋给 [3]int

比较两个数组是否相等

如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接通过较运算符(==!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。

1
2
3
4
5
6
7
8
9
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c)
// "true false false"

d := [3]int{1, 2}
fmt.Println(a == d)
// 编译错误:无法比较 [2]int == [3]int

遍历数组——访问每一个数组元素

1
2
3
for k, v := range team {
fmt.Println(k, v)
}

多维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var array_name [size1][size2]...[sizen] array_type

// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
array = [4][2]int{1: {0: 20}, 3: {1: 41}}

// 将 array1 的索引为 1 的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将数组中指定的整型值复制到新的整型变量里
var value int = array1[1][0]

其中,array_name 为数组的名字,array_type 为数组的类型,size1、size2 等等为数组每一维度的长度。

数组切片

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型),这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

Go语言中切片的内部结构包含地址、大小和容量,切片一般用于快速地操作一块数据集合,如果将数据集合比作切糕的话,切片就是你要的“那一块”,切的过程包含从哪里开始(切片的起始位置)及切多大(切片的大小),容量可以理解为装切片的口袋大小,如下图所示。

img

图:切片结构和内存分配

从数组或切片生成新的切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。

从连续内存区域生成切片是常见的操作,格式如下:

1
2
3
4
5
slice [开始位置 : 结束位置]

var a = [3]int{1, 2, 3}
fmt.Println(a, a[1:2])
//[1 2 3] [2]

语法说明如下:

  • slice:表示目标切片对象;
  • 开始位置:对应目标切片对象的索引;
  • 结束位置:对应目标切片的结束索引。

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置 - 开始位置;
  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
  • 当缺省开始位置时,表示从连续区域开头到结束位置;
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
  • 两者同时缺省时,与切片本身等效;
  • 两者同时为 0 时,等效于空切片,一般用于切片复位。

根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写 len(slice) 但不会报错。

表示原有的切片

生成切片的格式中,当开始和结束位置都被忽略时,生成的切片将表示和原切片一致的切片,并且生成的切片与原切片在数据内容上也是一致的,代码如下:

1
2
3
4
a := []int{1, 2, 3}
fmt.Println(a[:])

//[1 2 3]

重置切片,清空拥有的元素

把切片的开始和结束位置都设为 0 时,生成的切片将变空,代码如下:

1
2
3
4
a := []int{1, 2, 3}
fmt.Println(a[0:0])

//[]

直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var name []Type

// 声明字符串切片
var strList []string

// 声明整型切片
var numList []int

// 声明一个空切片
var numListEmpty = []int{}

// 输出3个切片
// 切片均没有任何元素,3 个切片输出元素内容均为空。
fmt.Println(strList, numList, numListEmpty)

// 输出3个切片大小
// 没有对切片进行任何操作,strList 和 numList 没有指向任何数组或者其他切片。
fmt.Println(len(strList), len(numList), len(numListEmpty))

// 切片判定空的结果
//声明但未使用的切片的默认值是 nil,strList 和 numList 也是 nil,所以和 nil 比较的结果是 true。
fmt.Println(strList == nil)
fmt.Println(numList == nil)
//numListEmpty 已经被分配到了内存,但没有元素,因此和 nil 比较时是 false。
fmt.Println(numListEmpty == nil)

/*
[] [] []
0 0 0
true
true
false
*/

切片是动态结构,只能与 nil 判定相等,不能互相判定相等。声明新的切片后,可以使用 append() 函数向切片中添加元素。

使用 make() 函数构造切片

如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:

1
2
3
4
5
6
7
8
9
10
make( []Type, size, cap )

a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b))
/*
[0 0] [0 0]
2 2
*/

其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。

其中 a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。

容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。

温馨提示

使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

切片操作

append()添加元素

1
2
3
4
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包

不过需要注意的是,在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。

切片长度 len 并不等于切片的容量 cap。

往一个切片中不断添加元素的过程,类似于公司搬家,公司发展初期,资金紧张,人员很少,所以只需要很小的房间即可容纳所有的员工,随着业务的拓展和收入的增加就需要扩充工位,但是办公地的大小是固定的,无法改变,因此公司只能选择搬家,每次搬家就需要将所有的人员转移到新的办公点。

  • 员工和工位就是切片中的元素。
  • 办公地就是分配好的内存。
  • 搬家就是重新分配内存。
  • 无论搬多少次家,公司名称始终不会变,代表外部使用切片的变量名不会修改。
  • 由于搬家后地址发生变化,因此内存“地址”也会有修改。

除了在切片的尾部追加,我们还可以在切片的开头添加元素:

1
2
3
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片

在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

因为 append 函数返回新切片的特性,所以切片也支持链式操作,我们可以将多个 append 操作组合起来,实现在切片中间插入元素:

1
2
3
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片

每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。

copy()切片复制

Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

1
copy( destSlice, srcSlice []T) int

其中 srcSlice 为数据来源切片,destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy() 函数的返回值表示实际发生复制的元素个数。

1
2
3
4
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

虽然通过循环复制切片元素更直接,不过内置的 copy() 函数使用起来更加方便,copy() 函数的第一个参数是要复制的目标 slice,第二个参数是源 slice,两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。

切片删除

从开头位置删除

删除开头的元素可以直接移动数据指针:

1
2
3
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):

1
2
3
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素

还可以用 copy() 函数来删除开头的元素:

1
2
3
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
从中间位置删除

对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:

1
2
3
4
5
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
从尾部删除
1
2
3
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素

代码的删除过程可以使用下图来描述。

img

图:切片删除元素的操作过程

Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来。

提示

连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。

循环迭代切片

1
2
3
4
5
6
7
8
9
10
11
12
// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 迭代每一个元素,并显示其值
for index, value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
/*
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40
*/

for index, value := range slice 的 index 和 value 分别用来接收 range 关键字返回的切片中每个元素的索引和值,这里的 index 和 value 不是固定的,读者也可以定义成其它的名字。

当迭代切片时,关键字 range 会返回两个值,第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本

使用 range 迭代切片会创建每个元素的副本

需要强调的是,range 返回的是每个元素的副本,而不是直接返回对该元素的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 迭代每个元素,并显示值和地址
for index, value := range slice {
fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])
}

/*
Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C
*/

因为迭代返回的变量是一个在迭代过程中根据切片依次赋值的新变量,所以 value 的地址总是相同的,要想获取每个元素的地址,需要使用切片变量和索引值(例如上面代码中的 &slice[index])。

如果不需要索引值,也可以使用下划线_来忽略这个值

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 迭代每个元素,并显示其值
for _, value := range slice {
fmt.Printf("Value: %d\n", value)
}

/*
Value: 10
Value: 20
Value: 30
Value: 40
*/

关键字 range 总是会从切片头部开始迭代。如果想对迭代做更多的控制,则可以使用传统的 for 循环

1
2
3
4
5
6
7
8
9
10
11
// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 从第三个元素开始迭代每个元素
for index := 2; index < len(slice); index++ {
fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}

/*
Index: 2 Value: 30
Index: 3 Value: 40
*/

多维切片

1
var sliceName [][]...[]sliceType

其中,sliceName 为切片的名字,sliceType为切片的类型,每个[ ]代表着一个维度,切片有几个维度就需要几个[ ]

1
2
3
4
//声明一个二维切片
var slice [][]int
//为二维切片赋值
slice = [][]int{{10}, {100, 200}}

上面的代码中展示了一个包含两个元素的外层切片,同时每个元素包又含一个内层的整型切片,切片 slice 的值如下图所示。

整型切片的切片的值

通过上图可以看到外层的切片包括两个元素,每个元素都是一个切片,第一个元素中的切片使用单个整数 10 来初始化,第二个元素中的切片包括两个整数,即 100 和 200。

这种组合可以让用户创建非常复杂且强大的数据结构,前面介绍过的关于内置函数 append() 的规则也可以应用到组合后的切片上,如下所示。

1
2
3
4
// 声明一个二维整型切片并赋值
slice := [][]int{{10}, {100, 200}}
// 为第一个切片追加值为 20 的元素
slice[0] = append(slice[0], 20)

Go语言里使用 append() 函数处理追加的方式很简明,先增长切片,再将新的整型切片赋值给外层切片的第一个元素,当上面代码中的操作完成后,再将切片复制到外层切片的索引为 0 的元素,如下图所示。

append 操作之后外层切片索引为 0 的元素的布局

map(映射)

map的声明

Go语言中 map 是一种特殊的数据结构,一种元素对(pair)的无序集合,pair 对应一个 key(索引)和一个 value(值),所以这个结构也称为关联数组或字典,这是一种能够快速寻找值的理想结构,给定 key,就可以迅速找到对应的 value。

map 这种数据结构在其他编程语言中也称为字典(Python)、hash 和 HashTable 等。

1
var mapname map[keytype]valuetype

其中:

  • mapname 为 map 的变量名。
  • keytype 为键类型。
  • valuetype 是键对应的值类型。

提示:[keytype] 和 valuetype 之间允许有空格。

在声明的时候不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,使用函数 len() 可以获取 map 中 pair 的数目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main
import "fmt"
func main() {
var mapLit map[string]int
//var mapCreated map[string]float32
var mapAssigned map[string]int
mapLit = map[string]int{"one": 1, "two": 2}
mapCreated := make(map[string]float32)
mapAssigned = mapLit
mapCreated["key1"] = 4.5
mapCreated["key2"] = 3.14159
mapAssigned["two"] = 3
fmt.Printf("Map literal at \"one\" is: %d\n", mapLit["one"])
fmt.Printf("Map created at \"key2\" is: %f\n", mapCreated["key2"])
fmt.Printf("Map assigned at \"two\" is: %d\n", mapLit["two"])
fmt.Printf("Map literal at \"ten\" is: %d\n", mapLit["ten"])
}

/*
Map literal at "one" is: 1
Map created at "key2" is: 3.14159
Map assigned at "two" is: 3
Map literal at "ten" is: 0
*/

示例中 mapLit 演示了使用{key1: value1, key2: value2}的格式来初始化 map ,就像数组和结构体一样。

上面代码中的 mapCreated 的创建方式mapCreated := make(map[string]float)等价于mapCreated := map[string]float{}

mapAssigned 是 mapList 的引用,对 mapAssigned 的修改也会影响到 mapLit 的值。

注意:可以使用 make(),但不能使用 new() 来构造 map,如果错误的使用 new() 分配了一个引用对象,会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址:

1
2
3
mapCreated := new(map[string]float)
//接下来当我们调用mapCreated["key1"] = 4.5的时候,编译器会报错:
//invalid operation: mapCreated["key1"] (index of type *map[string]float).

map的容量

和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:

1
2
3
make(map[keytype]valuetype, cap)

map2 := make(map[string]float, 100)

当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。

用切片作为map的值

既然一个 key 只能对应一个 value,而 value 又是一个原始类型,那么如果一个 key 要对应多个值怎么办?例如,当我们要处理 unix 机器上的所有进程,以父进程(pid 为整形)作为 key,所有的子进程(以所有子进程的 pid 组成的切片)作为 value。通过将 value 定义为 []int 类型或者其他类型的切片,就可以优雅的解决这个问题,示例代码如下所示

1
2
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

遍历map

map 的遍历过程使用 for range 循环完成,代码如下:

1
2
3
4
5
6
7
scene := make(map[string]int)
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
for k, v := range scene {
fmt.Println(k, v)
}

遍历对于Go语言的很多对象来说都是差不多的,直接使用 for range 语法即可,遍历时,可以同时获得键和值,如只遍历值,可以使用下面的形式:

1
2
3
4
//将不需要的键使用_改为匿名变量形式。
for _, v := range scene {}
//只遍历键时,使用下面的形式:(无须将值改为匿名变量形式,忽略值即可。)
for k := range scene {

如果需要特定顺序的遍历结果,正确的做法是先排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
// 声明一个切片保存map数据
var sceneList []string
// 将map数据遍历复制到切片中
for k := range scene {
sceneList = append(sceneList, k)
}
//对切片进行排序
//对 sceneList 字符串切片进行排序,排序时,sceneList 会被修改。
sort.Strings(sceneList)
// 输出
fmt.Println(sceneList)

/*
[brazil china route]
*/

删除和清空map元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
delete(map, 键)

scene := make(map[string]int)
// 准备map数据
scene["route"] = 66
scene["brazil"] = 4
scene["china"] = 960
delete(scene, "brazil")
for k, v := range scene {
fmt.Println(k, v)
}

/*
route 66
china 960
*/

有意思的是,Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。

sync.Map(在并发环境中使用的map)

Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建一个int到int的映射
m := make(map[int]int)

// 开启一段并发代码
go func() {
// 不停地对map进行写入
for {
m[1] = 1
}
}()

// 开启一段并发代码
go func() {
// 不停地对map进行读取
for {
_ = m[1]
}
}()

// 无限循环, 让并发程序在后台执行
for {
}

/*
fatal error: concurrent map read and map write
*/

错误信息显示,并发的 map 读和 map 写,也就是说使用了两个并发函数不断地对 map 进行读和写而发生了竞态问题,map 内部会对这种并发操作进行检查并提前发现。

需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

sync.Map 有以下特性:

  • 无须初始化,直接声明即可。
  • sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  • 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main
import (
"fmt"
"sync"
)
func main() {
var scene sync.Map
// 将键值对保存到sync.Map
scene.Store("greece", 97)
scene.Store("london", 100)
scene.Store("egypt", 200)
// 从sync.Map中根据键取值
fmt.Println(scene.Load("london"))
// 根据键删除对应的键值对
scene.Delete("london")
// 遍历所有sync.Map中的键值对
scene.Range(func(k, v interface{}) bool {
fmt.Println("iterate:", k, v)
return true
})
}

/*
100 true
iterate: egypt 200
iterate: greece 97
*/

代码说明如下:

  • var scene sync.Map,声明 scene,类型为 sync.Map,注意,sync.Map 不能使用 make 创建。
  • scene.Store("greece", 97),将一系列键值对保存到 sync.Map 中,sync.Map 将键和值以 interface{} 类型进行保存。
  • fmt.Println(scene.Load("london")),提供一个 sync.Map 的键给 scene.Load() 方法后将查询到键对应的值返回。
  • scene.Delete("london"),sync.Map 的 Delete 可以使用指定的键将对应的键值对删除。
  • scene.Range(func(k, v interface{}) bool {,Range() 方法可以遍历 sync.Map,遍历需要提供一个匿名函数,参数为 k、v,类型为 interface{},每次 Range() 在遍历一个元素时,都会调用这个匿名函数把结果返回。

sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。

list(列表)

list的声明

列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等。

list 的初始化有两种方法:分别是使用 New() 函数和 var 关键字声明,两种方法的初始化效果都是一致的。

1
2
3
4
//1) 通过 container/list 包的 New() 函数初始化 list
变量名 := list.New()
//通过 var 关键字声明初始化 list
var 变量名 list.List

列表与切片和 map 不同的是,列表并没有具体元素类型的限制,因此,列表的元素可以是任意类型,这既带来了便利,也引来一些问题,例如给列表中放入了一个 interface{} 类型的值,取出值后,如果要将 interface{} 转换为其他类型将会发生宕机。

在列表中插入元素

双链表支持从队列前方或后方插入元素,分别对应的方法是 PushFront 和 PushBack。

这两个方法都会返回一个 *list.Element 结构,如果在以后的使用中需要删除插入的元素,则只能通过 *list.Element 配合 Remove() 方法进行删除,这种方法可以让删除更加效率化,同时也是双链表特性之一。

1
2
3
l := list.New()
l.PushBack("fist")
l.PushFront(67)

列表插入元素的方法如下表所示。

方 法 功 能
InsertAfter(v interface {}, mark * Element) * Element 在 mark 点之后插入元素,mark 点由其他插入函数提供
InsertBefore(v interface {}, mark * Element) *Element 在 mark 点之前插入元素,mark 点由其他插入函数提供
PushBackList(other *List) 添加 other 列表元素到尾部
PushFrontList(other *List) 添加 other 列表元素到头部

从列表中删除元素

列表插入函数的返回值会提供一个 *list.Element 结构,这个结构记录着列表元素的值以及与其他节点之间的关系等信息,从列表中删除元素时,需要用到这个结构进行快速删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "container/list"
func main() {
l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
// 尾部添加后保存元素句柄
element := l.PushBack("fist")
// 在fist之后添加high
l.InsertAfter("high", element)
// 在fist之前添加noon
l.InsertBefore("noon", element)
// 使用
l.Remove(element)
}

下表中展示了每次操作后列表的实际元素情况。

操作内容 列表元素
l.PushBack(“canon”) canon
l.PushFront(67) 67, canon
element := l.PushBack(“fist”) 67, canon, fist
l.InsertAfter(“high”, element) 67, canon, fist, high
l.InsertBefore(“noon”, element) 67, canon, noon, fist, high
l.Remove(element) 67, canon, noon, high

遍历列表——访问列表的每一个元素

遍历双链表需要配合 Front() 函数获取头元素,遍历时只要元素不为空就可以继续进行,每一次遍历都会调用元素的 Next() 函数,代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}

/*
67
canon
*/

nil(空值/零值)

在Go语言中,布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串"",而指针、切片、映射、通道、函数和接口的零值则是 nil。

nil 是Go语言中一个预定义好的标识符,有过其他编程语言开发经验的开发者也许会把 nil 看作其他语言中的 null(NULL),其实这并不是完全正确的,因为Go语言中的 nil 和其他语言中的 null 有很多不同点。

nil 标识符是不能比较的

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
)
func main() {
fmt.Println(nil==nil)
}

/*
PS D:\code> go run .\main.go
# command-line-arguments
.\main.go:8:21: invalid operation: nil == nil (operator == not defined on nil)
*/

这点和 python 等动态语言是不同的,在 python 中,两个 None 值永远相等。

从上面的运行结果不难看出,==对于 nil 来说是一种未定义的操作。

nil 不是关键字或保留字

nil 并不是Go语言的关键字或者保留字,也就是说我们可以定义一个名称为 nil 的变量,比如下面这样:

1
var nil = errors.New("my god")

虽然上面的声明语句可以通过编译,但是并不提倡这么做。

nil 没有默认类型

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
)
func main() {
fmt.Printf("%T", nil)
print(nil)
}
/*
PS D:\code> go run .\main.go
# command-line-arguments
.\main.go:9:10: use of untyped nil
*/

不同类型 nil 的指针是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
)
func main() {
var arr []int
var num *int
fmt.Printf("%p\n", arr)
fmt.Printf("%p", num)
}
/*
PS D:\code> go run .\main.go
0x0
0x0
*/

通过运行结果可以看出 arr 和 num 的指针都是 0x0。

不同类型的 nil 是不能比较的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
"fmt"
)
func main() {
var m map[int]string
var ptr *int
fmt.Printf(m == ptr)
}
/*
PS D:\code> go run .\main.go
# command-line-arguments
.\main.go:10:20: invalid operation: arr == ptr (mismatched types []int and *int)
*/

两个相同类型的 nil 值也可能无法比较

在Go语言中 map、slice 和 function 类型的 nil 值不能比较,比较两个无法比较类型的值是非法的,下面的语句无法编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import (
"fmt"
)
func main() {
var s1 []int
var s2 []int
fmt.Printf(s1 == s2)
}
/*
PS D:\code> go run .\main.go
# command-line-arguments
.\main.go:10:19: invalid operation: s1 == s2 (slice can only be compared to nil)
*/

通过上面的错误提示可以看出,能够将上述不可比较类型的空值直接与 nil 标识符进行比较,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"fmt"
)
func main() {
var s1 []int
fmt.Println(s1 == nil)
}
/*
PS D:\code> go run .\main.go
true
*/

nil 是 map、slice、pointer、channel、func、interface 的零值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main
import (
"fmt"
)
func main() {
var m map[int]string
var ptr *int
var c chan int
var sl []int
var f func()
var i interface{}
fmt.Printf("%#v\n", m)
fmt.Printf("%#v\n", ptr)
fmt.Printf("%#v\n", c)
fmt.Printf("%#v\n", sl)
fmt.Printf("%#v\n", f)
fmt.Printf("%#v\n", i)
}
/*
PS D:\code> go run .\main.go
map[int]string(nil)
(*int)(nil)
(chan int)(nil)
[]int(nil)
(func())(nil)
<nil>
*/

零值是Go语言中变量在声明之后但是未初始化被赋予的该类型的一个默认值。

不同类型的 nil 值占用的内存大小可能是不一样的

一个类型的所有的值的内存布局都是一样的,nil 也不例外,nil 的大小与同类型中的非 nil 类型的大小是一样的。但是不同类型的 nil 值的大小可能不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import (
"fmt"
"unsafe"
)
func main() {
var p *struct{}
fmt.Println( unsafe.Sizeof( p ) ) // 8
var s []int
fmt.Println( unsafe.Sizeof( s ) ) // 24
var m map[int]bool
fmt.Println( unsafe.Sizeof( m ) ) // 8
var c chan string
fmt.Println( unsafe.Sizeof( c ) ) // 8
var f func()
fmt.Println( unsafe.Sizeof( f ) ) // 8
var i interface{}
fmt.Println( unsafe.Sizeof( i ) ) // 16
}

具体的大小取决于编译器和架构,上面打印的结果是在 64 位架构和标准编译器下完成的,对应 32 位的架构的,打印的大小将减半。

Mysql面试点整理

内容取自《程序员大彬》

https://mp.weixin.qq.com/s/VSsbUKEnILu1TbPjfwH8EA

事务的四大特性

事务特性ACID原子性Atomicity)、一致性Consistency)、隔离性Isolation)、持久性Durability)。

  • 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。
  • 一致性是指一个事务执行之前和执行之后都必须处于一致性状态。比如a与b账户共有1000块,两人之间转账之后无论成功还是失败,它们的账户总和还是1000。
  • 隔离性。跟隔离级别相关,如read committed,一个事务只能读到已经提交的修改。
  • 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

事务隔离级别有哪些?

先了解下几个概念:脏读、不可重复读、幻读。

  • 脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
  • 不可重读是指在对于数据库中的某行记录,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,另一个事务修改了数据并提交了。
  • 幻读是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,就像产生幻觉一样,这就是发生了幻读。

不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

幻读和不可重复读都是读取了另一条已经提交的事务,不同的是不可重复读的重点是修改,幻读的重点在于新增或者删除。

事务隔离就是为了解决上面提到的脏读、不可重复读、幻读这几个问题。

MySQL数据库为我们提供的四种隔离级别:

  • Serializable (串行化):通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。
  • Repeatable read (可重复读):MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,解决了不可重复读的问题。
  • Read committed (读已提交):一个事务只能看见已经提交事务所做的改变。可避免脏读的发生。
  • Read uncommitted (读未提交):所有事务都可以看到其他未提交事务的执行结果。

查看隔离级别:

1
select @@transaction_isolation;

设置隔离级别:

1
set session transaction isolation level read uncommitted;

什么是索引?

索引是存储引擎用于提高数据库表的访问速度的一种数据结构

索引的优缺点?

优点:

  • 加快数据查找的速度
  • 为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度
  • 加快表与表之间连接的速度

缺点:

  • 建立索引需要占用物理空间
  • 会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行动态维护索引,导致增删改时间变长

GO的学习日记(1)- Go的基本语法

本文章基于C语言中文网学习整理而来

http://c.biancheng.net/golang/syntax/

变量声明

1
2
//var name type
var a int = 1

Go语言的基本类型有

  • bool
  • string
  • int(int8、int16、int32、int64)
  • uint(uint8、uint16、uint32、uint64、uintptr)
  • byte(uint8别名)
  • rune(int32的别名,代表一个Unicode码)
  • float32、float64
  • complex64、complex128

声明变量时会自动赋予基于该类型的0值

int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil 等。

所有的内存在 Go 中都是经过初始化的。

批量声明与简短声明

1
2
3
4
5
6
7
8
9
10
11
12
//批量声明
var (
a int
b string
c []float32
d func() bool
e struct{x int}
)

//简短声明
i,j := 0,1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//在标准格式基础上,如果将后面的的格式省略,编译器会自动推导变量的类型
var hp = 100

/*
第 1 和 2 行,右值为整型,attack 和 defence 变量的类型为 int。
第 3 行,表达式的右值中使用了 0.17。所以这里如果不指定 damageRate 变量的类型,Go语言编译器会将 damageRate 类型推导为 float64,我们这里不需要 float64 的精度,所以需要强制指定类型为 float32。
第 4 行,将 attack 和 defence 相减后的数值结果依然为整型,使用 float32() 将结果转换为 float32 类型,再与 float32 类型的 damageRate 相乘后,damage 类型也是 float32 类型。
*/
var attack = 40
var defence = 20
var damageRate float32 = 0.17
var damage = float32(attack-defence) * damageRate
fmt.Println(damage)
//3.4 [(40-20)*0.17]

多重赋值与匿名变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//go语言的多重赋值
var a int = 100
var b int = 200
b, a = a, b
fmt.Println(a, b)

//匿名变量:匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。
func GetData() (int, int) {
return 100, 200
}
func main(){
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)
}

数据类型

整形int

大多数情况下,我们只需要 int 一种整型即可,它可以用于循环计数器(for 循环中控制循环次数的变量)、数组和切片的索引,以及任何通用目的的整型运算符,通常 int 类型的处理速度也是最快的。

用来表示 Unicode 字符的 rune 类型和 int32 类型是等价的,通常用于表示一个 Unicode 码点。这两个名称可以互换使用。同样,byte 和 uint8 也是等价类型,byte 类型一般用于强调数值是一个原始的数据而不是一个小的整数。

最后,还有一种无符号的整数类型 uintptr,它没有指定具体的 bit 大小但是足以容纳指针。uintptr 类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。

尽管在某些特定的运行环境下 int、uint 和 uintptr 的大小可能相等,但是它们依然是不同的类型,比如 int 和 int32,虽然 int 类型的大小也可能是 32 bit,但是在需要把 int 类型当做 int32 类型使用的时候必须显示的对类型进行转换,反之亦然。

哪些情况下使用 int 和 uint

程序逻辑对整型范围没有特殊需求。例如,对象的长度使用内建 len() 函数返回,这个长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用 int 来表示。

反之,在二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用 int 和 uint。

浮点型float

  • 常量 math.MaxFloat32 表示 float32 能取到的最大数值,大约是 3.4e38;
  • 常量 math.MaxFloat64 表示 float64 能取到的最大数值,大约是 1.8e308;
  • float32 和 float64 能表示的最小值分别为 1.4e-45 和 4.9e-324。

一个 float32 类型的浮点数可以提供大约 6 个十进制数的精度,而 float64 则可以提供约 15 个十进制数的精度,通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大。

1
2
3
4
5
6
7
8
9
10
var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1) // "true"!

//声明规则
const e = .71828 // 0.71828
const f = 1. // 1

//很小或很大的数最好用科学计数法书写,通过 e 或 E 来指定指数部分:
const Avogadro = 6.02214129e23 // 阿伏伽德罗常数
const Planck = 6.62606957e-34 // 普朗克常数
1
2
3
4
5
6
7
8
9
//文字输出
func main() {
fmt.Printf("%f\n", math.Pi)
fmt.Printf("%.2f\n", math.Pi)
}
/*
3.141593
3.14
*/

语言复数

在计算机中,复数是由两个浮点数表示的,其中一个表示实部(real),一个表示虚部(imag)。

Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。

复数的值由三部分组成 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,RE 和 IM 均为 float 类型,而最后的 i 是虚数单位。

1
var name complex128 = complex(x,y)

其中 name 为复数的变量名,complex128 为复数的类型,“=”后面的 complex 为Go语言的内置函数用于为复数赋值,x、y 分别表示构成该复数的两个 float64 类型的数值,x 为实部,y 为虚部。

1
2
3
4
5
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"

布尔类型bool

&&的优先级比||高(&& 对应逻辑乘法,|| 对应逻辑加法,乘法比加法优先级要高)

Go语言并不会隐性转换布尔值为1/0

字符串string

一个字符串是一个不可改变的字节序列,字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。

UTF-8 是一种被广泛使用的编码格式,是文本文件的标准编码,其中包括 XML 和 JSON 在内也都使用该编码。由于该编码对占用字节长度的不定性,在Go语言中字符串也可能根据需要占用 1 至 4 个字节,这与其它编程语言如 C++Java 或者 Python 不同(Java 始终使用 2 个字节)。Go语言这样做不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。

可以使用双引号""来定义字符串,字符串中可以使用转义字符来实现换行、缩进等效果,常用的转义字符包括:

  • \n:换行符
  • \r:回车符
  • \t:tab 键
  • \u 或 \U:Unicode 字符
  • \:反斜杠自身

一般的比较运算符(==、!=、<、<=、>=、>)是通过在内存中按字节比较来实现字符串比较的,因此比较的结果是字符串自然编码的顺序。字符串所占的字节长度可以通过函数 len() 来获取,例如 len(str)。

字符串的内容(纯字节)可以通过标准索引法来获取,在方括号[ ]内写入索引,索引从 0 开始计数:

  • 字符串 str 的第 1 个字节:str[0]
  • 第 i 个字节:str[i - 1]
  • 最后 1 个字节:str[len(str)-1]

字符串拼接

两个字符串 s1 和 s2 可以通过 s := s1 + s2 拼接在一起。将 s2 追加到 s1 尾部并生成一个新的字符串 s。

定义多行字符串

在Go语言中,使用双引号书写字符串的方式是字符串常见表达方式之一,被称为字符串字面量(string literal),这种双引号字面量不能跨行,如果想要在源码中嵌入一个多行字符串时,就必须使用`反引号,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
const str = `第一行
第二行
第三行
\r\n
`
fmt.Println(str)

/*
第一行
第二行
第三行
\r\n
*/

在这种方式下,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

字符类型byte和rune

Go语言的字符有以下两种:

  • 一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。
  • 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。

byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题,例如 var ch byte = ‘A’,字符使用单引号括起来。

1
2
3
//在 ASCII 码表中,A 的值是 65,使用 16 进制表示则为 41,所以下面的写法是等效的:
var ch byte = 65var ch byte = '\x41'
//(\x 总是紧跟着长度为 2 的 16 进制数)

Go语言同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。

在书写 Unicode 字符时,需要在 16 进制数之前加上前缀\u或者\U。因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则使用\u前缀,如果需要使用到 8 个字节,则使用\U前缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U", ch, ch2, ch3) // UTF-8 code point

/*
65 - 946 - 1053236
A - β - r
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234
*/

格式化说明符%c用于表示字符,当和字符配合使用时,%v%d会输出用于表示该字符的整数,%U输出格式为 U+hhhh 的字符串。

Unicode 包中内置了一些用于测试字符的函数,这些函数的返回值都是一个布尔值,如下所示(其中 ch 代表字符):

  • 判断是否为字母:unicode.IsLetter(ch)
  • 判断是否为数字:unicode.IsDigit(ch)
  • 判断是否为空白符号:unicode.IsSpace(ch)

UTF-8 和 Unicode 有何区别?

Unicode 与 ASCII 类似,都是一种字符集。

字符集为每个字符分配一个唯一的 ID,我们使用到的所有字符在 Unicode 字符集中都有一个唯一的 ID,例如上面例子中的 a 在 Unicode 与 ASCII 中的编码都是 97。汉字“你”在 Unicode 中的编码为 20320,在不同国家的字符集中,字符所对应的 ID 也会不同。而无论任何情况下,Unicode 中的字符的 ID 都是不会变化的。

UTF-8 是编码规则,将 Unicode 中字符的 ID 以某种方式进行编码,UTF-8 的是一种变长编码规则,从 1 到 4 个字节不等。编码规则如下:

  • 0xxxxxx 表示文字符号 0~127,兼容 ASCII 字符集。
  • 从 128 到 0x10ffff 表示其他字符。

根据这个规则,拉丁文语系的字符编码一般情况下每个字符占用一个字节,而中文每个字符占用 3 个字节。

广义的 Unicode 指的是一个标准,它定义了字符集及编码规则,即 Unicode 字符集和 UTF-8、UTF-16 编码等。

数据类型转换

1
2
a := 5.0
b := int(a)

当从一个取值范围较大的类型转换到取值范围较小的类型时(将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。

只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)

16 位有符号整型的范围是 -32768~32767,而变量 a 的值 1047483647 不在这个范围内。1047483647 对应的十六进制为 0x3e6f54ff,转为 int16 类型后,长度缩短一半,也就是在十六进制上砍掉一半,变成 0x54ff,对应的十进制值为 21759。

Go语言指针

Go语言为程序员提供了控制数据结构指针的能力,但是,并不能进行指针运算。Go语言允许你控制特定集合的数据结构、分配的数量以及内存访问模式,这对于构建运行良好的系统是非常重要的。指针对于性能的影响不言而喻,如果你想要做系统编程、操作系统或者网络应用,指针更是不可或缺的一部分。

指针(pointer)在Go语言中可以被拆分为两个核心概念:

  • 类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
  • 切片,由指向起始元素的原始指针、元素数量和容量组成。

受益于这样的约束和拆分,Go语言的指针类型变量即拥有指针高效访问的特点,又不会发生指针偏移,从而避免了非法修改关键性数据的问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。

切片比原始指针具备更强大的特性,而且更为安全。切片在发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。

认识指针地址和指针类型

一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 48 个字节,占用字节的大小与所指向的值的大小无关。当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。指针变量通常缩写为 ptr。

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:

1
ptr := &v

其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为*T,称做 T 的指针类型,*代表指针。

当使用&操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*操作符,也就是指针取值。

*操作符作为右值时,意义是取指针的值,作为左值时,也就是放在赋值操作符的左边时,表示 a 指针指向的变量。其实归纳起来,*操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。

如果在 swap() 函数中交换操作的是指针值,会发生什么情况?可以参考下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func swap(a, b *int) {
b, a = a, b
}

func main() {
x, y := 1, 2
swap(&x, &y)
fmt.Println(x, y)
}

// 1 2

结果表明,交换是不成功的。上面代码中的 swap() 函数交换的是 a 和 b 的地址,在交换完毕后,a 和 b 的变量值确实被交换。但和 a、b 关联的两个变量并没有实际关联。这就像写有两座房子的卡片放在桌上一字摊开,交换两座房子的卡片后并不会对两座房子有任何影响。

创建指针的另一种方法——new() 函数

Go语言还提供了另外一种方法来创建指针变量

1
2
3
str := new(string)
*str = "Go语言教程"
fmt.Println(*str)

new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。

语言变量的生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。

变量的生命周期与变量的作用域有着不可分割的联系:

  • 全局变量:它的生命周期和整个程序的运行周期是一致的;
  • 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;
  • 形式参数和函数返回值:它们都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。
1
2
3
4
5
6
7
8
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(
size+int(x*size+0.5), size+int(y*size+0.5),
blackIndex, // 最后插入的逗号不会导致编译错误,这是Go编译器的一个特性
) // 小括号另起一行缩进,和大括号的风格保存一致
}

上面代码中,在每次循环的开始会创建临时变量 t,然后在每次循环迭代中创建临时变量 x 和 y。临时变量 x、y 存放在栈中,随着函数执行结束(执行遇到最后一个}),释放其内存。

堆和栈的区别

  • 堆(heap):堆是用于存放进程执行中被动态分配的内存段。它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态加入到堆上(堆被扩张)。当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减);
  • 栈(stack):栈又称堆栈, 用来存放程序暂时创建的局部变量,也就是我们函数的大括号{ }中定义的局部变量。

在程序的编译阶段,编译器会根据实际情况自动选择在栈或者堆上分配局部变量的存储空间,不论使用 var 还是 new 关键字声明变量都不会影响编译器的选择。

1
2
3
4
5
6
7
8
9
10
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}

上述代码中,函数 f 里的变量 x 必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。用Go语言的术语说,这个局部变量 x 从函数 f 中逃逸了。

相反,当函数 g 返回时,变量 y 不再被使用,也就是说可以马上被回收的。因此,y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间,也可以选择在堆上分配,然后由Go语言的 GC(垃圾回收机制)回收这个变量的内存空间。

在实际的开发中,并不需要刻意的实现变量的逃逸行为,因为逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。

虽然Go语言能够帮助我们完成对内存的分配和释放,但是为了能够开发出高性能的应用我们任然需要了解变量的声明周期。例如,如果将局部变量赋值给全局变量,将会阻止 GC 对这个局部变量的回收,导致不必要的内存占用,从而影响程序的性能。

常量声明

声明规则与变量声明一致

1
const pi = 3.14159

Go语言中的常量使用关键字 const 定义,用于存储不会改变的数据,常量是在编译时被创建的,即使定义在函数内部也是如此,并且只能是布尔型、数字型(整数型、浮点型和复数)和字符串型。由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。

常量的值必须是能够在编译时就能够确定的,可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

  • 正确的做法:const c1 = 2/3
  • 错误的做法:const c2 = getNumber() // 引发构建错误: getNumber() 用做值

所有常量的运算都可以在编译期完成,这样不仅可以减少运行时的工作,也方便其他代码的编译优化,当操作数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。

因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度:

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式,对应的常量类型也是一样的。例如:

1
2
3
4
5
6
7
8
const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d)
// "1 1 2 2"

iota常量生成器

常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一。

【示例 1】首先定义一个 Weekday 命名类型,然后为一周的每天定义了一个常量,从周日 0 开始。在其它编程语言中,这种类型一般被称为枚举类型。

1
2
3
4
5
6
7
8
9
10
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)

周日将对应 0,周一为 1,以此类推。

无类型常量

Go语言的常量有个不同寻常之处。虽然一个常量可以有任意一个确定的基础类型,例如 int 或 float64,或者是类似 time.Duration 这样的基础类型,但是许多常量并没有一个明确的基础类型。

编译器为这些没有明确的基础类型的数字常量提供比基础类型更高精度的算术运算,可以认为至少有 256bit 的运算精度。这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。

通过延迟明确常量的具体类型,不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。

1
2
3
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

如果 math.Pi 被确定为特定类型,比如 float64,那么结果精度可能会不一样,同时对于需要 float32 或 complex128 类型值的地方则需要一个明确的强制类型转换:

1
2
3
4
const Pi64 float64 = math.Pi
var x float32 = float32(Pi64)
var y float64 = Pi64
var z complex128 = complex128(Pi64)

对于常量面值,不同的写法可能会对应不同的类型。例如 0、0.0、0i 和 \u0000 虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true 和 false 也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。

type关键字(类型别名)

类型别名是 Go 1.9 版本添加的新功能,主要用于解决代码升级、迁移中存在的类型兼容性问题。在 C/C++ 语言中,代码重构升级可以使用宏快速定义一段新的代码,Go语言中没有选择加入宏,而是解决了重构中最麻烦的类型名变更问题。

在 Go 1.9 版本之前定义内建类型的代码是这样写的:

1
2
type byte uint8
type rune int32

而在 Go 1.9 版本之后变为:

1
2
type byte = uint8
type rune = int32

这个修改就是配合类型别名而进行的修改。

区分类型别名与类型定义

定义类型别名的写法为:

1
type TypeAlias = Type

类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型,就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import (
"fmt"
)
// 将NewInt定义为int类型
type NewInt int
// 将int取一个别名叫IntAlias
type IntAlias = int
func main() {
// 将a声明为NewInt类型
var a NewInt
// 查看a的类型名
fmt.Printf("a type: %T\n", a)
// 将a2声明为IntAlias类型
var a2 IntAlias
// 查看a2的类型名
fmt.Printf("a2 type: %T\n", a2)
}

/*
a type: main.NewInt
a2 type: int
*/
  • type NewInt int ,将 NewInt 定义为 int 类型,这是常见的定义类型的方法,通过 type 关键字的定义,NewInt 会形成一种新的类型,NewInt 本身依然具备 int 类型的特性。
  • type IntAlias = int,将 IntAlias 设置为 int 的一个别名,使用 IntAlias 与 int 等效。
  • var a NewInt,将 a 声明为 NewInt 类型,此时若打印,则 a 的值为 0。
  • fmt.Printf("a type: %T\n", a),使用%T格式化参数,打印变量 a 本身的类型。
  • var a2 IntAlias,将 a2 声明为 IntAlias 类型,此时打印 a2 的值为 0。
  • fmt.Printf("a2 type: %T\n", a2),打印 a2 变量的类型。

结果显示 a 的类型是 main.NewInt,表示 main 包下定义的 NewInt 类型,a2 类型是 int,IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型。

非本地类型不能定义方法

1
2
3
4
5
6
7
8
9
10
11
package main
import (
"time"
)
// 定义time.Duration的别名为MyDuration
type MyDuration = time.Duration
// 为MyDuration添加一个函数
func (m MyDuration) EasySet(a string) {
}
func main() {
}

编译上面代码报错,信息如下:

cannot define new methods on non-local type time.Duration

编译器提示:不能在一个非本地的类型 time.Duration 上定义新方法,非本地类型指的就是 time.Duration 不是在 main 包中定义的,而是在 time 包中定义的,与 main 包不在同一个包中,因此不能为不在一个包中的类型定义方法。

解决这个问题有下面两种方法:

  • 将第type MyDuration = time.Duration修改为type MyDuration time.Duration,也就是将 MyDuration 从别名改为类型;
  • 将 MyDuration 的别名定义放在 time 包中

在结构体成员嵌入时使用别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main
import (
"fmt"
"reflect"
)
// 定义商标结构
type Brand struct {
}
// 为商标结构添加Show()方法
func (t Brand) Show() {
}
// 为Brand定义一个别名FakeBrand
type FakeBrand = Brand
// 定义车辆结构
type Vehicle struct {
// 嵌入两个结构
FakeBrand
Brand
}
func main() {
// 声明变量a为车辆类型
var a Vehicle

// 指定调用FakeBrand的Show
a.FakeBrand.Show()
// 取a的类型反射对象
ta := reflect.TypeOf(a)
// 遍历a的所有成员
for i := 0; i < ta.NumField(); i++ {
// a的成员信息
f := ta.Field(i)
// 打印成员的字段名和类型
fmt.Printf("FieldName: %v, FieldType: %v\n", f.Name, f.Type.
Name())
}
}
1
2
FieldName: FakeBrand, FieldType: Brand
FieldName: Brand, FieldType: Brand

代码说明如下:

  • type Brand struct,定义商标结构。
  • func (t Brand) Show(),为商标结构添加 Show() 方法。
  • type FakeBrand = Brand,为 Brand 定义一个别名 FakeBrand。
  • type Vehicle struct{FakeBrand Brand},定义车辆结构 Vehicle,嵌入 FakeBrand 和 Brand 结构。
  • var a Vehicle,将 Vechicle 实例化为 a。
  • a.FakeBrand.Show(),显式调用 Vehicle 中 FakeBrand 的 Show() 方法。
  • ta := reflect.TypeOf(a),使用反射取变量 a 的反射类型对象,以查看其成员类型。
  • 遍历 a 的结构体成员。
  • fmt.Printf("FieldName: %v, FieldType: %v\n", f.Name, f.Type.,打印 Vehicle 类型所有成员的信息。

这个例子中,FakeBrand 是 Brand 的一个别名,在 Vehicle 中嵌入 FakeBrand 和 Brand 并不意味着嵌入两个 Brand,FakeBrand 的类型会以名字的方式保留在 Vehicle 的成员中。

如果尝试将第 33 行改为:

1
a.Show()

编译器将发生报错:

1
ambiguous selector a.Show

在调用 Show() 方法时,因为两个类型都有 Show() 方法,会发生歧义,证明 FakeBrand 的本质确实是 Brand 类型。

标识符

标识符是指Go语言对各种变量、方法、函数等命名时使用的字符序列,标识符由若干个字母、下划线_、和数字组成,且第一个字符必须是字母。通俗的讲就是凡可以自己定义的名称都可以叫做标识符。

下划线_是一个特殊的标识符,称为空白标识符,它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用_作为变量对其它变量进行赋值或运算。

在使用标识符之前必须进行声明,声明一个标识符就是将这个标识符与常量、类型、变量、函数或者代码包绑定在一起。在同一个代码块内标识符的名称不能重复。

标识符的命名需要遵守以下规则:

  • 由 26 个英文字母、0~9、_组成;
  • 不能以数字开头,例如 var 1num int 是错误的;
  • Go语言中严格区分大小写;
  • 标识符不能包含空格;
  • 不能以系统保留关键字作为标识符,比如 break,if 等等。

命名标识符时还需要注意以下几点:

  • 标识符的命名要尽量采取简短且有意义;
  • 不能和标准库中的包名重复;
  • 为变量、函数、常量命名时采用驼峰命名法,例如 stuName、getVal;

当然Go语言中的变量、函数、常量名称的首字母也可以大写,如果首字母大写,则表示它可以被其它的包访问(类似于 Java 中的 public);如果首字母小写,则表示它只能在本包中使用 (类似于 Java 中 private)。

在Go语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:

append bool byte cap close complex complex64 complex128 uint16
copy false float32 float64 imag int int8 int16 uint32
int32 int64 iota len make new nil panic uint64
print println real recover string true uint uint8 uintptr

预定义标识符一共有 36 个,主要包含Go语言中的基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。

运算优先级

优先级 分类 运算符 结合性
1 逗号运算符 , 从左到右
2 赋值运算符 =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= 从右到左
3 逻辑或 || 从左到右
4 逻辑与 && 从左到右
5 按位或 | 从左到右
6 按位异或 ^ 从左到右
7 按位与 & 从左到右
8 相等/不等 ==、!= 从左到右
9 关系运算符 <、<=、>、>= 从左到右
10 位移运算符 <<、>> 从左到右
11 加法/减法 +、- 从左到右
12 乘法/除法/取余 *(乘号)、/、% 从左到右
13 单目运算符 !、*(指针)、& 、++、–、+(正号)、-(负号) 从右到左
14 后缀运算符 ( )、[ ]、-> 从左到右

注意:优先级值越大,表示优先级越高。